1.认识时间复杂度以及比较排序算法

本文探讨了常数时间操作的概念,并详细分析了选择排序、冒泡排序、插入排序和快速排序的时间复杂度。重点讲解了归并排序和快速排序的优化版本,如荷兰国旗问题和快排3.0。通过实例和Master公式,揭示了递归算法的时间复杂度计算方法。
摘要由CSDN通过智能技术生成

时间复杂度

常数时间的操作:

一个操作如果和数据量无关,每次操作都是固定时间内完成的操作,叫做常数操作。比如取数组元素操作、加减乘除操作都属于常数操作。

什么叫时间复杂度?

举个例子:选择排序。
选择排序的步骤:

for i = 0 to N-1:
	遍历一遍i到N-1的数组,从数组中找到最小的元素的索引d;
	将d号元素与第i号元素位置交换;

动画展示:selection sort

在这个流程中,有多少个常数操作?

通过计算,快速排序的常数操作总共进行了 N ( N − 1 ) − 2 N(N-1)-2 N(N1)2次。
也就是 a N 2 + b N + c aN^2+bN+c aN2+bN+c次,所谓的时间复杂度就是指:只保留最高阶的常数次操作的项,且忽略掉最高阶系数剩下的东西。也就是 N 2 N^2 N2,简写成 O ( N 2 ) O(N^2) O(N2)

时间复杂度用于评估一个算法的优劣。

冒泡排序的时间复杂度分析

冒泡排序就是:多次遍历数组,相邻元素两两交换,将最大值元素放到最后
动画演示:Bubble Sort
冒泡排序的步骤:

for i=0 to N-1:
	flag=false;		//用于记录本次遍历是否有交换
	for j=0 to N-1-i:
		if A[j]>A[j+1]:
			flag=true;
			swap(A[j],A[j+1]);
	if flag==flase:	//没有交换则表示数组已经排好序了。
		break;

Bubble_sort.cpp

序号取数比较交换次数
1211N-1
2211N-2
N-12111

总计需要: 3 × ( N − 1 + N − 2 + ⋯ + 1 ) = 3 × N ( N − 1 ) 2 3\times(N-1+N-2+\cdots+1)=3\times\frac{N(N-1)}{2} 3×(N1+N2++1)=3×2N(N1),所以冒泡排序的时间复杂度为 O ( N 2 ) O(N^2) O(N2)

题外话:
异或运算: 可以将异或运算看成是没有进位的加法运算
异或运算的性质:
<1>. a ^ 0 = a; a ^ a=0;
<2>. 满足交换律和结合律:即a ^ b = b ^ a; a ^ b ^ c = a ^ (b ^ c)
异或运算的面试题
一堆数组中有一个数字出现了奇数次,其他数字出现了偶数次,如何找到这个出现奇数次的数字?
答:将数组中所有元素进行异或运算,假设a是出现奇数次的数字,x是除去一个a后的数组,由于x内都是出现偶数次的数据,所以x内部的异或运算结果为0,a ^ x = a。
一堆数组中有两个数字出现了奇数次,其他数字出现了偶数次,如何找到这个出现奇数次的数字?
答:将数组中所有元素进行异或运算,假设a和b是出现奇数次的数字,那么最终的异或结果一定是a ^ b且 ≠ 0 \ne0 =0。假设a ^ b=0100 0010。(结果中一定不全为0,那些不为0的部分就是a和b的差异部分),找到不为0的其中一位,比如第1位。然后将原数组分成两类数据:(1).第1位为0的集合P (2).和第1位为1的集合Q。对集合P和Q分别进行异或,最终结果就是要求的两个数。思路:寻找二者差异,进行分类,达到降维的目的
取a最右侧不为0的数:
#include <bitset>

cout << bitset<8>(a&(~a + 1)) << endl;

插入排序的时间复杂度分析

插入排序就是:确保0 ~ i范围上数组是有序的,从i+1号元素开始,为了确保0 ~ i+1上数组有序,需要向前交换,当停止交换时,0 ~ i+1就是有序的
动画演示:insertion sort

for i=1 to N-1:
	for j=i to 1:
		if A[j]<A[j-1]:
			swap(A[j], A[j-1]);
		else
			break;

insertion_sort.cpp

对于插入排序,数据状况的不同,会导致时间复杂度的不同。
比如
完全逆序的数组 [ 5 , 4 , 3 , 2 , 1 ] [5,4,3,2,1] [5,4,3,2,1],插入排序的时间复杂度为 O ( N 2 ) O(N^2) O(N2)
完全顺序的数组 [ 1 , 2 , 3 , 4 , 5 ] [1,2,3,4,5] [1,2,3,4,5],时间复杂度为 O ( N ) O(N) O(N)
时间复杂度看的是最差的情况下的级别,所以,插入排序的时间复杂度为 O ( N 2 ) O(N^2) O(N2)

递归的时间复杂度评估:Matser 公式

递归就是指:调用函数自身,使问题规模变小的行为
比如,查找一串数组中的最大值的递归算法如下:

int mmax(int* const A, const int low, const int high)
{
    if (low == high)
        return A[low];
    int mid = (low+high)>>1;
    int max_left = mmax(A, low, mid);
    int max_right = mmax(A, mid+1, high);
    return max_left>max_right ? max_left : max_right;
}

假设输入5个元素的数组 [ 3 , 1 , 2 , 5 , 4 ] [3,1,2,5,4] [3,1,2,5,4]
当第一次调用该函数时,将m(0,4)入栈。
然后从m(0,4)中划分出两个子问题:m(0,2)和m(3,4),先将m(0,2)入栈。
然后从m(0,2)中划分出两个子问题:m(0,1)和m(2,2),先将m(0,1)入栈。
然后从m(0,1)中划分出两个子问题:m(0,0)和m(1,1),将m(0,0)入栈。
此时,m(0,0)可以求出结果,出栈,然后将m(1,1)入栈。

Master公式:
T ( N ) = a T ( N b ) + O ( N d ) T(N)=aT(\frac{N}{b})+O(N^d) T(N)=aT(bN)+O(Nd)

那么上面递归的例子的Master公式就是: 2 T ( N 2 ) + O ( 1 ) 2T(\frac{N}{2})+O(1) 2T(2N)+O(1)
Master公式和时间复杂度的关系:
{ O ( N d ) , l o g b a < d O ( N l o g b a ) , l o g b a > d O ( N d ∗ l o g 2 N ) , l o g b a = = d \begin{cases} O(N^d), & log_ba<d \\ O(N^{log_ba}), & log_ba>d \\ O(N^d*log_2N), & log_ba==d \end{cases} O(Nd),O(Nlogba),O(Ndlog2N),logba<dlogba>dlogba==d
由于上面的例子中 l o g 2 2 > 0 log_22>0 log22>0,所以时间复杂度为 O ( N ) O(N) O(N)

归并排序的时间复杂度

归并排序是指:将数组左右两个子数组A1和A2分别排好序,然后创建一块和源数组同样大小的辅助数组空间,分别将两端较小的元素拷贝到辅助数组中,得到排好序的辅助数组,然后将辅助数组拷贝回源数组中。
归并排序动画演示

sort(A,0,n-1);
void sort(A, low, high)
{
	if(low>=high)
		return;
	int mid = (low+high)/2
	sort(A,low, mid);
	sort(A,mid+1,high);
	for(i=low,j=mid+1,d=low;d<=high&&i<=mid&&j<=high;)
	{
		if(A[i]<A[j])
			B[d++]=A[i++];
		else
			B[d++]=A[j++];
	}
	while(i<=mid)
		B[d++]=A[i++];
	while(j<=high)
		B[d++]=A[j++];
	for(d=low;d<=high;d++)
		A[d]=B[d];
}

merge_sort.cpp
根据递归过程得到该递归的master公式: T ( N ) = 2 T ( N 2 ) + O ( N ) T(N)=2T(\frac{N}{2})+O(N) T(N)=2T(2N)+O(N)
也就是a=2,b=2,d=1。由于 l o g b a = 1 log_ba=1 logba=1, d = 1 d=1 d=1所以该递归的时间复杂度为 N l o g 2 N Nlog_2N Nlog2N

由Merge_sort改写的面试题每年必出!

面试题:小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和,求一个数组的小和,要求时间复杂度小于 O ( N 2 ) O(N^2) O(N2)
例如[1,3,4,2,5],1左边比1小的数,没有;3左边比3小的数,1;4左边比4小的数,1,3;2左边比2小的数,1;5左边比5小的数,1,3,4,2;
所以该数组的小和为1+1+3+1+1+3+4+2=16。

分析:
当一个问题比较复杂时,寻找与其等价的问题。
寻找左边小 ↔ \leftrightarrow 寻找右边大。而且寻找右边大可以精确地知道有几个比自己大。
例如,1右边比1大的个数:4个1;3右边比3大的个数:2个3;4右边比4大的个数:1个4;2右边比2大的个数:1个2;5右边比5大的个数:没有。所以最终的小和为4*1+2*3+1*4+1*2=16。
在这里插入图片描述
将一个大问题划分成两个小问题,两个小问题的数组对应的小和已知,并且已经排好序,合并两个小问题时就可以知道大问题的小和。xiaohe.cpp
最终的时间复杂度为 O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N)

逆序对问题:
在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请输出数组中总逆序对的数量。
比如:[3,2,4,5,1]。
对于3来说的逆序对包括:(3,2),(3,1)
对于2来说的逆序对包括:(2,1)
对于4来说的逆序对包括:(4,1)
对于5来说的逆序对包括:(5,1)
总计5对逆序对。

分析:
找逆序等价于找右边比自己小的数。其他参考上面的问题。

快速排序的时间复杂度

问题1:
给定一个数组A和常数num。要求数组中所有小于等于num的元素都放在大于num元素的左边,整体不要求有序。
比如:A=[3,1,2,5,4],num=3。ans=[1,2,3,5,4]
分析:我们假设,刚开始小于等于3的区域在数组最左侧,
在这里插入图片描述
然后遍历数组中的元素,不断扩充小于等于3的区域:
(1). 如果元素A[i] ≤ \le 3,则将该元素与区域右侧元素交换,i++
(2). 如果元素A[i] > \gt > 3,i++
最终会将数组划分成3块区域,就是不断地探索未知区域的过程。
在这里插入图片描述

p=0	//p表示"<=区域"的下标
for(i=0;i<N;i++)
	if(A[i]<=num)
		swap(A[i],A[p++]);

问题2:
荷兰国旗问题:
给定一个数组A,一个常数num。将所有小于num的元素放在数组左边,等于num的元素放在数组中间,大于num的元素放在数组右边,整体不要求有序。
分析:假设,"小于num区域"在数组最左侧,"大于num区域"在数组最右侧。遍历数组:
(1). 如果 A [ i ] < n u m A[i]<num A[i]<num ,将A[i]和"小于num区域"右侧元素交换,该区域右扩p++,i++。
(2). 如果 A [ i ] = = n u m A[i]==num A[i]==num ,i++
(3). 如果 A [ i ] > n u m A[i]>num A[i]>num ,将A[i]和"大于num区域"左侧元素交换,该区域左扩q- -。
在这里插入图片描述

p=0,q=N	//p表示"<num区域"下标,q表示">num区域下标"
i=0
while(i<q)
	if(A[i]<num)
		swap(A[i],A[p++])
		i++
	else if(A[i]>num)
		swap(A[i],A[--q])
	else
		i++

快速排序1.0
将数组的第0号元素当成num,再在剩下的元素中进行区域划分,划分成"<num区域"和">=num区域"。
在这里插入图片描述
然后将0号元素和"<num区域"最后的元素交换
在这里插入图片描述
然后,左侧区域和右侧区域分别执行相同的操作。
quick_sort1.cpp

快速排序2.0
利用荷兰国旗问题,将整个数组划分成3个区域:
在这里插入图片描述

,这样"==num区域"这一批数据就排好序了。
quick_sort2.cpp

快速排序的时间复杂度为 O ( N 2 ) O(N^2) O(N2)
因为可以举出最差的情况的例子:[1,2,3,4,5]
这是由于每次取划分值都取的太偏了。如果每次取划分值都在中间的位置,那么递归过程就变成了 T ( N ) = T ( N 2 ) + T ( N 2 ) + O ( N ) T(N)=T(\frac{N}{2})+T(\frac{N}{2})+O(N) T(N)=T(2N)+T(2N)+O(N),即时间复杂度为 O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N)

快排3.0
每次将中间值与第low号元素交换,然后执行划分。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值