各大经典排序算法剖析

快速排序

快速排序是一种就地排序,会改变输入数组,同时快速排序是一种不稳定的排序。快速排序的关键点是选择一个合适的轴点(pivot),这个选取的不确定性也导致了每次数据的分割是不均匀的。

对于选取的一个轴点,其左边的都不大于轴点,右边的都不小于轴点: [low, pivot-1] <= pivot <= [pivot+1, high]

int partation(std::vector<int> &arr,int left,int right){
	//第一步,随机选取一个数作为pivot,这里我们选择第一个数即可
	int p = left;
	std::swap(arr[p],arr[right]); //交换arr[p],arr[right],此时arr[right]等于我们选择的pivot
	int i=left;
	for(int j=left;j<right;++j){
		if(arr[j]<=arr[right]){
            std::swap(arr[i],arr[j]);
            ++i;
        }
        //定义i,j=left,让j进行循环,如果啊arr[j]<=arr[r],说明当前数字应该在pivot的左边,所以交换i和j,并i++,当循环完这个操作,i,j不用再交换意味着在left到i这个区间全部小于pivot,i+1到right-1,这个区间全是大于pivot,此时我们将arr[i]和arr[r](即最初的pivot)交换,即成功的将其进行了划分,并且i就是我们的pivot的位置
		  
	}
	std::swap(arr[i],arr[right]);
    return i;
}

void quickSort(std::vector<int> &arr,int left,int right){
    //结束条件,当数组中只有一个数自然是有序的,即right<=left
    if(right<=left)
        return;
    int p = partation(arr,left,right);
    quickSort(arr,left,p-1);
    quickSort(arr,p+1,right);
}

画个草图示意一下划分过程
在这里插入图片描述
partition 算法,就是在扫描数据的过程中,将所有大于轴点的数据放到轴点右边,将所有小于轴点的数据放到左边。而quickSort 算法就是不断的分割,当只有一个数据时,其本身就是有序,再将所有的子问题组合起来,思想是“分而治之”。

复杂度

可在线性时间内将原向量排序问题划分为两个相互独立、总体规模保持线性的子向量排序问题。但是分治策略高效的两个必要条件:

  • 子任务划分的高效性及子任务之间的独立性:满足
  • 子任务规模的接近:不满足。

实际上,不仅仅不满足条件 2,还是有可能相差悬殊。这是因为划分所得子序列的长度与划分的具体过程无关,而是取决于轴点。比如轴点 rank=r ,那么子向量的规模是 rn-1-r。当r=0,左侧子向量规模为空,右侧与原来的向量等长,对称即 r=n-1。这对于有序向量而言,每次都是简单的选取最左侧元素作为轴点,此时的效率是 O(n^2)

降低最坏概率情况

不能每次固定的选取最左端元素,可以每次自由随机选取一个元素

平均运行时间

最差的时间复杂度是 O(n^2),但在平均意义下,时间复杂度是 O(1.38logn)

应对退化情况

当所有的元素都是重复的时候,此时对应的情况即之前的最坏情况.因为循环中的 pivot <= arr[right]会一直满足,直到 low < high 不满足,最后整个算法退化为线性递归,时间复杂度是o(n^2)
改进如下:

int partation(std::vector<int>& arr, int L, int R) { 
	int pivot = arr[L];
	
	while(L < R) { 
	  while(L < R && pivot  <= arr[R]) --R; if(L < R) arr[L++] = arr[R];
	  while(L < R && arr[L] <= pivot ) ++L; if(L < R) arr[R--] = arr[L];
	}
	
	arr[L] = pivot;
	
	return L;
}

如此,每次遇到重复的元素,都是将元素放到的对立的区间,当循环中止的时候,恰好位于中间的位置,此时的时间复杂度最好,是O(nlogn)

继续优化

我们知道,在序列基本有序的情况下快速排序的时间复杂度会退化到O(n^2),原因就是轴点的选取的不当导致一共需要n层,每层的时间复杂度为O(n),所以此时我们就优化基本有序的情况就是选择一个随机数做为轴点,以下

int partation(vector<int> &arr,int l,int r){
    int p = rand()%(r-l+1)+l;
	swap(arr[p],arr[r]);
    int i=l,j=l;
    for(;j<r;j++){
        if(arr[j]<arr[r]) swap(arr[i++],arr[j]);
    }
    swap(arr[i],arr[r]);
    return i;
}
暂时更到这

mergeSort

归并排序分为两个部分,先将整个序列化为2个规模一样的序列,然后排序。递归的调用,不断的划分,直到子序列的规模为1,那么这个规模为1的子序列就是有序的(是不是与快排的思想类似)

void mergeSort(vector<int> &arr,int lo,int mid,int hi){
	if(hi-lo<2) return;
	int mid = (lo+hi)>>1;
	mergeSort(arr,lo,mid);
	mergeSort(arr,mid,hi);
	merge(arr,lo,mid,hi);
}
  • 递归基:当hi-lo==1时,就是只有一个元素,此时有序,返回
  • 中点:每次都是取中点,将数据区间划分为两个等规模区间
  • 归并:当划分到单独元素时,开始合并数据
#define inf 0x3fffff

void merge(vector<int> &arr,int lo,int mid,int hi) {
    //复制左边半部分
	vector<int> l(arr.begin()+lo,arr.begin()+mid);
	l.push_back(inf);
    //复制右边部分,注意一下这种复制是左闭右开,所以是arr首地址+hi+1
	vector<int> r(arr.begin()+mid,arr.begin()+hi+1);
	r.push_back(inf);
	int i=0,j=0,k=lo;
	while(k<=hi) {
		if(l[i]<=r[j]) arr[k++]=l[i++];
		else arr[k++]=r[j++];
	}
}

归并排序我看别人写的merge部分都比较臃肿,判断条件太多,因此我进行了一点优化,即在左右两个子序列引入一个无穷大的哨兵,这样就不用判断i,j是否超出范围

冒泡排序

冒泡排序是排序中得基本算法,时间复杂度O(n^2)

消除逆序对

冒泡法,每一趟遍历都会消除一些逆序对,让最大值元素归位。比如第一趟遍历让最大值元素归位,第二趟让第二大元素归位,一直到第n趟让第n大,也就是最小的元素归位。

void bubbleSort(std::vector<int> &arr){
	if(arr.empty) return ;
	int r = arr.size()-1;
	for(int l=0,l<r--;l++)//r--的意思就是最大值已经沉底,不用再去比较,能省一点时间
		for(int l=0;l<r;l++){
			if(arr[l]>arr[l+1])
                std::swap(arr[l],arrr[l+1]);
		}
}

这其实也是一种贪心的思想体现:因为只要将遇到的所有“逆序对”消除,那么就是一个有序的序列,而冒泡排序就是每遇到一个逆序对就修正这个逆序对,消除他,通过n趟的迭代,那么就能消除所有的逆序对,就会变得有序。

时间复杂度

毫无疑问是O(n2)。即使是一个有序的序列,用冒泡法时间复杂度依然是O(n2)。因为在每一轮迭代中,只是考虑逆序对,将最大未归位元素归位。可是如果前面的部分元素已经归位,那么是否可以直接跳过而不再迭代???

void bubbleSort(std::vector<int> &arr){
	if(arr.empty) return ;
	int r = arr.size()-1;
	for(int l=0,l<r--;l++)//r--的意思就是最大值已经沉底,不用再去比较,能省一点时间
		{int last = 0;
		for(int l=0;l<r;l++){
			if(arr[l]>arr[l+1]){
                std::swap(arr[l],arrr[l+1]);
                last=l;//记录最后一次交换元素的位置
                }
		}
		r=last;//下次直接从最后一次更新元素位置开始
	}
}

如此不仅在序列本身有序时时间复杂度仅为O(n),也为普通情况下进行了改善,不再是每次都遍历,而是直接跳到最后一次交换元素的位置。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值