四大排序及其时间、空间复杂度分析

时间复杂度

在算法的分析中,语句的执行次数T(n)是一个关于n(问题规模)的一个函数。分析n的变化引起T(n)的改变,进而得到T(n)的数量级,也就是时间频率。如果存在某一个辅助函数f(n),当n趋于无穷大时,T(n)/f(n)的值为一个不为0的常数,有T(n)=O(f(n)),这就是算法的渐进时间复杂度,也就是我们常说的时间复杂度

大O表示法:用O(f(n))来体现时间复杂度的方法被称作大O表示法;
大O推导法
O(1)叫做常数阶;O(n)叫做线性阶;O(n^2)叫做平方阶。

  1. 用常数1取代运行时间中的所有加法常数。
  2. 在修改后的运行次数函数中,只保留最高阶项。
  3. 如果最高阶项存在且不是1,则去除与这个项相乘的常数。得到的结果就是大O阶。

举一个简单的例子

int i;
for(i=0;i<n;i++)//该语句的复杂度为O(n)
{
    
    
	cout<<i;    //该语句的复杂度为O(1);
}

 
 
  
  
  • 1
  • 2
  • 3
  • 4
  • 5

这段代码的时间复杂度为O(n),其中“cout<<i”的执行次数为1,是常量阶,所以它的复杂度为O(1);“for(i=0;i<n;i++)”的执行次数是n,是线性阶,所以它的复杂度为O(n)。整段代码的复杂度为O(1*n),也就是O(n).

常见的时间复杂度:
常数阶O(1),

如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,
其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)

对数阶O(log2 n),

线性阶O(n),

线性对数阶O(n log2 n),

平方阶O(n^2)

立方阶O(n^3)

k次方阶O(n^K),

指数阶O(2^n)

其他时间复杂度都会随着n的变化慢慢变大,算法开销也越来越大 。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

常见的时间复杂度所损耗时间排序:
O(1)<O(log n)<O(n)<O(nlogn)<O(n ^2)<O(n ^3)<O(2 ^n)<O(n!)<O(n ^n)

空间复杂度

空间复杂度指的是一个程序在执行时,所占有的临时内存空间大小:S(n)=O(f(n));n是问题的规模,f(n)为语句关于n所占据的内存的函数。

举个简单的例子:
交换两个变量的值,它需要定义一个临时的变量,这就造成了空间复杂度,因为是常量阶,所以它的空间复杂度为O(1).

	int i = 10, j = 100;//两个需要交换的值
	 //交换变量的值
	int temp=i;//定义临时变量
	i=j;
	j=temp;

 
 
  
  
  • 1
  • 2
  • 3
  • 4
  • 5

提到空间复杂度,就需要提一下递归了,在进行递归的算法的时候,每一次的递归都会使用临时的变量保存递归的信息,所以递归的方法很消耗内存,也就是空间复杂度相对较高。如果递归次数过多,会造成内存超载,无法计算得到需要的数据。所以,当循环的次数过多的时候,尽量不要使用递归方法。

插入排序

原理分析

将一个记录插入到已排好序的序列中,从而得到一个新的有序序列

将序列的第一个数据看成是一个有序的子序列,然后从第二个记录逐个向该有序的子序列进行有序的插入,直至整个序列有序

代码实现

#include<iostream>
using namespace std;

void printArray(int *arr, int len)
{
for (int i = 0; i < len; ++i)
{
cout << arr[i] << " ";
}
cout << endl;
}

void InsertSort(int *arr, int len)
{

<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">int</span> i <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> len<span class="token punctuation">;</span> <span class="token operator">++</span>i<span class="token punctuation">)</span>
<span class="token punctuation">{</span>

	<span class="token keyword">if</span> <span class="token punctuation">(</span>arr<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">&gt;</span> arr<span class="token punctuation">[</span>i <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
	<span class="token punctuation">{</span>
		<span class="token keyword">int</span> temp <span class="token operator">=</span> arr<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">;</span>
		<span class="token keyword">int</span> j <span class="token operator">=</span> i <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">;</span>
		<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token punctuation">;</span> j <span class="token operator">&gt;=</span> <span class="token number">0</span> <span class="token operator">&amp;&amp;</span> temp <span class="token operator">&gt;</span> arr<span class="token punctuation">[</span>j<span class="token punctuation">]</span><span class="token punctuation">;</span> j<span class="token operator">--</span><span class="token punctuation">)</span>        
		<span class="token punctuation">{</span>
			arr<span class="token punctuation">[</span>j <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">=</span> arr<span class="token punctuation">[</span>j<span class="token punctuation">]</span><span class="token punctuation">;</span>
		<span class="token punctuation">}</span>
		arr<span class="token punctuation">[</span>j <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">=</span> temp<span class="token punctuation">;</span>
	<span class="token punctuation">}</span>
<span class="token punctuation">}</span>

}

int main()
{
int arr[] = { 4,5,8,9,1,2 };
int len = sizeof(arr) / sizeof(int);//通过字节计算数组大小
printArray(arr, len);
InsertSort(arr, len);
printArray(arr, len);
return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

复杂度计算

1、当初始序列为正序时,只需要外循环n-1次,每次进行一次比较,无需移动元素。此时比较次数(C min)和移动次数(M min)达到最小值。
C min=n-1;
M min=0;

此时时间复杂度为O(n).
2、初始序列为反序时,需要外循环n-1次,每次排序中待插入的元素都要和[0,i-1]中的i个元素进行比较且要将这i个元素后移i次,此时比较次数和移动次数达到最大值。
C min=1+2+3+4+……+n-1=(n-1)n/2;
M min=1+2+3+……+n-1=(n-1)n/2;

此时时间复杂度为O(n^2).
3、在直接插入排序中只使用了i,j,tmp这三个辅助元素,与问题规模无关,空间复杂度为O(1)

冒泡排序

概念及思路

重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端,故称为"冒泡排序"。

代码实现

#include<iostream>
using namespace std;
void BubbleSort(int *a, int size)
{
    
    
 for (int i = 0; i < size; i++)//外循环,循环每个元素
 {
    
    
  for (int j = 1; j < size - i; j++)//内循环进行元素的两两比较
  {
    
    
   if (a[j] < a[j - 1])//判断相邻元素并进行交换
   {
    
    
    int temp = a[j];
    a[j] = a[j - 1];
    a[j - 1] = temp;
   }
  }
 }
}
int main()
{
    
    
 int a[10] = {
    
     2, 7, 34, 54, 12, 5, 19, 33, 88, 23 };
 cout << "原来的数组为:" << endl;
 for (int i = 0; i < 10; i++)
 {
    
    
  cout << a[i] << " ";
 }
 cout << endl;
 BubbleSort(a, 10);
 cout << "冒泡排序后的数组为:" << endl;
 for (int i = 0; i < 10; i++)
 {
    
    
  cout << a[i] << " ";
 }
 return 0;
}

 
 
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

时间复杂度

外循环和内循环以及判断和交换元素的时间开销。
最优的情况也就是开始就已经排序好序了,那么就可以不用交换元素了,由于外层循环为n,内层所需要循环比较的次数为(n-1)、(n-2)…1由等差数列求和得时间花销为:[ n(n-1) ] / 2;所以最优的情况时间复杂度为:O( n^2 )
最差的情况也就是开始的时候元素是逆序的,那么每一次排序都要交换两个元素,则时间花销为:[ 3n(n-1) ] / 2;(其中比上面最优的情况所花的时间就是在于交换元素的三个步骤);所以最差的情况下时间复杂度为:O( n^2 )

空间复杂度

冒泡排序的辅助变量空间仅仅是一个临时变量,并且不会随着排序规模的扩大而进行改变,所以空间复杂度为O(1)。

选择排序

void Efferve()
{
    
    
 	int m[5] = {
    
     12, 8, 6, 9, 10 };
	 int max = m[0];
	 for (int i = 0; i < 4; i++)
	 {
    
    
  		for (int j = i; j < 4; j++)
 		 {
    
    
 		 	 if (m[j] < m[j + 1])
  			 {
    
    
    				max = m[j + 1];
   				 m[j + 1] = m[j];
    				m[j] = max;
 			  }
 		 }
	 } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

上面的代码可以看出选择排序套用了两个循环如下:

 for (int i = 0; i < 4; i++)
  {
    
    
    for (int j = i; j < 4; j++)
    {
    
    
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

当i=0下面循环4次,每次i+1下面循环都执行次4-i次,因此上述循环次数为T=(4-1))+ (4 -2)+(4 - 3)+ 1;
T=[4*(4-1)]/2次
哪当N个数进行排序时,将进行T=[N*(N-1)]/2次,根据计算方法保留最高次N ^2,因此选择排序的时间复杂度为O(N ^2);

因为排序中始终只用到了数组大小的空间,为常数,因此空间复杂度为O(1)。

快速排序

基本思路

在一个数组中,找一个数为基准数,将这个数中所有比基准数大的数放在该数的右边,比基准数小的数放在该数的左边。

例如"6 1 2 7 9 3 4 5 10 8"这个数组
以6作为基准数,将比6小的数放在6的组左边,比6大的数放在6的右边
得到:3 1 2 5 4 6 9 7 10 8
可以看出6的左边的数都比6小,而右边的数都比6大,此时6已经归位

具体步骤为:

  1. 先找一个基准数(一般为第一个),然后从右边开始向左找,找到第一个小于基准值的数,然后从左开始找,找到第一个小于基准值的数,然后进行交换,一直到左边和右边相遇时,则将基准数与找到的位置进行交换
  2. 以基准数为分界线,划分为左右2个数组,再以步骤1进行递归
  3. 当递归的数组中无法再继续递归时,循序则结束,此时的数组则已经完成了排序

代码实现

void QuickSort(int *n, int left, int right)
{
    
    
	if (left > right)
		return ;
	int temp = n[left];					//temp中存的数为基准数
	int i = left, j = right;
	int t;
	while (i != j)
	{
    
    
		//一定要先从右向左找
		while (i < j && n[j] >= temp)
			j--;
		while (i < j && n[i] <= temp)
			i++;
		if (i < j)
		{
    
    
			t = n[i];
			n[i] = n[j];
			n[j] = t;
		}
	}
	n[left] = n[i];
	n[i] = temp;
	QuickSort(n, left, i - 1);			//处理左边的数
	QuickSort(n, i + 1, right);			//处理右边的数
}

 
 
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

复杂度计算

空间复杂度:logn
主要是由于递归造成的栈空间的使用,
最好的情况下其树的深度为:log2(n)
空间复杂度为 O(logn)
而最坏的情况下:需要n-1次调用,每2个数都需要交换,此时退化为冒泡排序
空间复杂度为 O(n)
平均时间复杂度为:O(logn)

时间复杂度:O(nlogn)
由于快速排序用到了递归调用,因此计算其时间复杂度也需要用到递归算法计算
递归算法的时间复杂度公式:T[n] = aT[n/b] + f(n)
**

最优情况下时间复杂度
快速排序最优的情况就是每一次取到的元素都刚好平分整个数组

此时的时间复杂度公式则为:T[n] = 2T[n/2] + f(n);T[n/2]为平分后的子数组的时间复杂度,f[n] 为平分这个数组时所花的时间
第一次递归:
T[n] = 2T[n/2] + n;
第二次递归:
令 n = n/2
= 2^2 T[ n/ (2^2) ] + 2n
第三次递归:
令:n = n/(2^2)
= 2^3 T[ n/ (2^3) ] + 3n

第m次递归:
令:n = n/( 2^(m-1) )
= 2^m T[1] + mn

当最后平分的不能再平分时,也就是说把公式一直往下跌倒,到最后得到T[1]时,说明这个公式已经迭代完了(T[1]是常量了)。
得到:T[n/ (2^m) ] = T[1] ===>> n = 2^m ====>> m = logn;
T[n] = 2^m T[1] + mn ;其中m = logn;
T[n] = 2^(logn)T[1]+lognn = n +nlogn
又因为当n >= 2时:nlogn >= n (也就是logn > 1),所以取后面的 nlogn;

综上所述:快速排序最优的情况下时间复杂度为:O( nlogn )

最差情况下时间复杂度
最差的情况就是每一次取到的元素就是数组中最小/最大的,这种情况其实就是冒泡排序了(每一次都排好一个元素的顺序)
此时的时间复杂度为:T[n] = n * (n-1) = n^2 + n;
综上所述:快速排序最差的情况下时间复杂度为:O( n^2 )

平均时间复杂度
快速排序的平均时间复杂度也是:O(nlogn)

猜你喜欢

转载自blog.csdn.net/Wu_0526/article/details/107959363