关于八大排序算法(C++)

本文针对leetcode912:排序数组作一个排序的总结:题目链接
本题最快使用桶排序,归并排序也可以很快通过,所以这两个排序方法强烈推荐。

——————修改版本2020/7/5 :添加一些易错点和原理解释。——————修改版本2020/8/16:添加关于稳定性分类和复杂度归类对比。

排序稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
——百度百科

算法按照稳定性为分类,共分为两类:

  • 稳定排序算法
    冒泡排序 插入排序 桶排序 归并排序
  • 不稳定排序算法
    选择排序 希尔排序 堆排序 快速排序

附加说明:理解选择排序的稳定性

选择类排序:每一轮可以定下来一个数的位置

简单选择排序

时间复杂度:O(n^2)
空间复杂度:O(1)

vector<int> SelectSort(vector<int>& nums) {
   for(int i=0;i<nums.size();i++){
       int mini_num=i; //作为一个key
       //与key后面每个value进行比较
       for(int j=i+1;j<nums.size();j++) 
       {
           if(nums[mini_num]>nums[j]){
               mini_num=j;
           }
       }
       if(mini_num!=i){
           swap(nums[mini_num],nums[i]);
       }
   }
   return nums;
}

堆排序

原理:
#1.先改造成大顶堆,从最后一个子树开始调整。最后一棵子树的父节点为:i=nums.size()/2-1 (i从0开始)。
#2.调整好之后,把最顶端的节点与最后节点调换,此时最大值确定。
#3.确定最大值之后,需要将除最后节点的剩下树继续调整为大顶堆,以后以此类推。

时间复杂度:O(n*log(n)),稳定
空间复杂度:O(1)

vector<int> HeapSort(vector<int>& nums){
 //建堆,从第一个非叶子结点开始调整大顶堆,非叶节点是n/2-1    
	for(int i=nums.size()/2-1;i>=0;i--)
	{
	   adjust(nums,i,nums.size());
	}
 //将最顶端的最大值nums[0]与最后叶子nums[n-1]交换,再对最顶端进行调整
	for(int i=nums.size()-1;i>0;i--){
	   swap(nums[i],nums[0]);
	   adjust(nums,0,i); //这里的size不同
	}
	return nums;
    }
vector<int> adjust(vector<int>& nums,int n,int size){
	int leftchild=2*n+1;
	int rightchild=2*n+2;
	int largest=n;
//比较以n为父亲的二叉子树最大值
	if(leftchild<size && nums[leftchild]>nums[largest]){
	    largest=leftchild;
	}
	if(rightchild<size&& nums[rightchild]>nums[largest]){
	    largest=rightchild;//易错点1:这里的nums[largest]容易写成nums[n],这里的largest可能已经变了。
	}
	if(largest!=n){
	    swap(nums[largest],nums[n]);
 //这里很容易错,交换以后想继续对以前的那个节点做排序,但是交换以后它是largest,而不是n
	    adjust(nums,largest,size);
	}
	return nums;        
    }

交换/插入类排序:常常比较冒泡和快排之间的差别

冒泡排序

时间复杂度: O(n^2)
如果有flag标志可以是O(1),适用于当一开始序列就已经依次排好的情况。
空间复杂度: O(1)

vector<int> BubbleSort(vector<int>& nums) {
	 int flag;
	 for(int i=nums.size()-1;i>=0;i--){
	     flag=0;
	     for(int j=1;j<=i;j++){
	         if(nums[j-1]>nums[j]){
	             flag=1;
	             swap(nums[j-1],nums[j]);
	         }
	     }
	     if(flag==0){
	         return nums;
	     }
	 }
	 return nums;
	}

快排

时间复杂度:最优为O(n*log(n)),最差为O(n^2),不稳定
空间复杂度:O(1)

算法原理与比较:

快速排序,选出一个枢纽元素,将这个枢纽元素和数组所有元素进行比较,把彼枢纽元素大的元素放在枢纽元素右边,把比枢纽元素小的放在枢纽元素左边,
而对于一趟快速排序要比较n次,每一趟快排的时间复杂度是O(n)
接下来你要对快排划分出来的两个子数组进行递归快排,如果每一趟快排很平均的将数组划分为两个子数组,那么递归快排的深度就是O(logn),所以总的时间复杂度就是O(nlogn)。
但是快排可以退化成冒泡,一旦每一趟快排,不幸的选择出最大或最小元素作为枢纽元素,那么递归深度将变成n,则时间复杂度变成了O(n^2),此时快排的效率降到最低,退化为冒泡。
所以快排对于枢纽元素的选择上很关键,如果能选择出每趟平均划分数组的枢纽元素,那么快排的效率最高,如何选择枢纽元素将成为衡量快排的关键,可以使用三者取中法来选择,每趟快排前,先将数组开始位置,中间位置,以及结尾位置的三个元素进行比较,选择其中的中间大的数做为枢纽元素,这样可以降低退化成冒泡的风险
代码中选择较简单的方法:随机取数组中的一个数做为枢纽元素,这样也可以降低风险。

C++代码:

vector<int> quicksort(int left, int right,vector<int>& nums)
    {     
       if(left>=right){
           return nums;
       }
       int low=left;
       int high=right;
       //随机取key值,降低风险
       swap(nums[rand()%right+1],nums[left]);
       int key=nums[left];
       while(left<right){
       //易错点1:key值的比较,等号应该跟着right,因为key值取的是nums[left],如果等号跟着left,则左边的那个比较相当于失效了。
           for(;left<right&&nums[right]>=key;right--){;} 
           for(;left<right&&nums[left]<key;left++){;} 
           swap(nums[left],nums[right]);
       }
       quicksort(low,left,nums);
       quicksort(left+1,high,nums);
       return nums;
    }

简单插入排序

时间复杂度:O(n^2)
空间复杂度:O(1)

vector<int> insertSort(vector<int>& nums)
    {
        for(int i=1;i<nums.size();i++){
            for(int j=i;j>0;j--){
                if(nums[j]<nums[j-1]){
                    swap(nums[j],nums[j-1]);
                }
            }
        }
        return nums;
    }

希尔排序

时间复杂度:未解问题
空间复杂度:O(1)

vector<int> ShellSort(vector<int>& nums){
	for(int gap=nums.size()/2;gap>0;gap/=2){
	//增量最后为1,下面两个循环容易错
	    for(int i=gap;i<nums.size();i++){
	        for(int j=i;j-gap>=0;j-=gap)
	        {                 
	            if(nums[j]<nums[j-gap])
	            swap(nums[j],nums[j-gap]);                   
	        }
	    }
	}
	return nums;
	}

归并排序:又稳定又快

分治+合并,算法示意图:
在这里插入图片描述时间复杂度:O(n*log(n)) ,稳定
空间复杂度:O(n)

vector<int> mergeSort(int left,int right, vector<int>& nums)
    {
        if(left>right){return {};}
        if(left==right){return {nums[left]};}
        int mid=(left+right)/2; //易错点1:mid的地方应该基于left
        //分治
        auto ln=mergeSort(left,mid,nums);
        auto rn=mergeSort(mid+1,right,nums);
        vector<int> res;
        int i=0,j=0;
        while(i<ln.size()&&j<rn.size()){
            if(ln[i]<rn[j]){ //易错点2:不是nums[i],而是ln[i],很容易写错
            	//合并
                res.push_back(ln[i++]);
            }
            else{
                res.push_back(rn[j++]);
            }
        }
        while(i<ln.size()) {res.push_back(ln[i++]);}
        while(j<rn.size()) {res.push_back(rn[j++]);}
        return res;
    }

计数排序/桶排序:很神!超快!

举例:现在给了待排序的数组[5, 3, 5, 2, 8]。那么:
#1.申请(8-2+1)共7个桶用于放元素,桶为:0~6号桶 ( 后面得到的旗子位置记得加上low:2)。
#2.遍历待排序的数组[5, 3, 5, 2, 8],把对应的桶里面放入小旗子,即 2-2=0、3-2=1、8-2=6 号桶各放了一个旗子, 5-2=3号 桶里放了两个旗子。
#3.从 0 到 6 遍历一遍所有的桶,如果桶里没有小旗子就跳过,有小旗子就往结果里放入小旗子个数的该元素,加上最小值2,得到[2, 3, 5, 5, 8]。

时间复杂度:O(n)
空间复杂度:O(n)
特点:时间复杂度低,但是空间复杂度高。而且在排序前需要提前知道一些信息,比如排序的大致范围。

vector<int> basketSort(vector<int>& nums){
    int low=*min_element(nums.begin(),nums.end());
    int high=*max_element(nums.begin(),nums.end());
    int size=high-low+1;
    //画桶
    int basket[size];    
    for(int i=0;i<size;i++){
            basket[i]=0;
    }
    for (int i=0;i<nums.size();i++){
        basket[nums[i]-small]++; //易错点1:这里是-small,不是+small
    }
    vector<int> res;
    for(int i=0;i<n;i++){
        while(basket[i]>0){ //易错点2:这里的basket需要递减,否则nums中相同值只会出现一次。
             res.push_back(i+small);
             basket[i]--;}
    }
    return res;
}

各类排序算法复杂度对比:

在这里插入图片描述

稳定性分析

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

复杂度分析

时间复杂度指执行算法所需要的计算工作量。
空间复杂度指算法在计算机内执行时所需存储空间的度量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值