数据结构之排序算法

135 阅读7分钟

image.png

1、插入排序

插入排序的基本操作就是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录增1的有序表。

算法步骤:

  • 将第一个元素看作有序序列,其余元素作为未排序序列
  • 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。

image.png

代码实现

    vector<int> insertSort(vector<int> nums)  //nums为待排序数组
    {
        vector<int> ans;//有序数组
        ans.resize(nums.size());
        for(int i = 0;i<nums.size();i++)
        {
            int k;
    	    for (k=i;k>0;k--)// 从后向前比较
            {	        
    		if ( ans[k-1] > nums[i] )    //nums[i]前面的数比它大
                    ans[k]=ans[k-1];         //将大数向后移动
    		else break; //* 找到插入的位置,退出
            }
            ans[k] = nums[i];  /* 完成插入操作 */
        }
        return ans;//返回排好序的数组
    }

2、快速排序

快速排序的基本思想是:通过一趟排序将待排数组分割成独立的两部分,其中一部分数组存放的值较小,而另一部分存放的值较大,然后在对这两部分分别做上述操作,知道整个数组有序。

算法步骤

  • 在数组中选取一个值,将该值作为枢轴(pivot)
  • 扫描数组,将比pivot小的数放到pivot左边,比pivot大的数放到pivot右边,此时数组以pivot为中心分为两部分
  • 分别对左右两部分进行上述分割操作,直到整个数组有序

image.png

代码实现

void Quick_Sort(vector<int>& arr, int begin, int end)
    {
        if(begin > end)//结束条件
            return;
        int pivot = arr[begin];//以子序列的第一个元素作为pivot
        int left = begin;
        int right = end;
        while(left < right)
        {
            while(arr[right] >= pivot && right > left)//从右往左,找到一个比pivot小的数
                right--;
            while(arr[left] <= pivot && right > left)//从左往右,找到一个比pivot大的数
                left++;
            if(left < right)//交换上面找到的两个值的位置
            {
                int temp = arr[left];
                arr[left] = arr[right];
                arr[right] = temp;
            }
        }//循环结束后left即pivot在序列中应插入的位置
        arr[begin] = arr[left];
        arr[left] = pivot;//将pivot放到中间位置
        Quick_Sort(arr, begin, left-1);//分别对子序列排序
        Quick_Sort(arr, left+1, end);
    }

快速排序的优化

  • 优先选择枢轴 在上述代码实现中,枢轴总是固定选择序列的第一个元素,但如果该值太大或太小,都会造成大量无效的比较,大大影响排序的性能,对于基本有序的序列,选取固定位置的元素作为枢轴就成为极不合理的做法。

改进方法:随机选取枢轴,在某种程度上解决了对于基本有序序列排序的性能瓶颈,但是比较凭运气,万一还是随机选到了很大或很小的数,该怎么办?

再次改进:三数取中,取三个数排序后取中间值作为枢轴,一般选取左端、右端和中间的三个数。这样的话至少中间这个数不会是最大数或最小数,枢轴位于较为中间的值的可能性就大大提高了。但是对于非常大的待排序序列,还是不足以保证选取出一个好的枢轴,因此又出现了九数取中法,三个数为一组,取三组得到三个中数,在从三个中数中选取中间值作为枢轴。

  • 优化小数组时的排序方案 如果待排序数组特别小,快速排序反而不如直接插入排序,其原因在于快速排序中用到了递归,对大量数据进行排序时,这点性能影响对于它的整体算法优势可以忽略,但当数组只有几个记录需要排序时,就有点大炮打蚊子的感觉。

改进方法:当high-low不大于某个常数时,就用直接插入排序,这样可以保证最大化利用两种排序的优势来完成排序工作。

3、堆排序

要进行堆排序首先要清楚什么是堆,对就是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右子结点的值,成为大顶堆;每个节点的值都小于或等于其左右子结点的值,称为小顶堆。

堆排序的基本思想就是,将待排序的序列构造成一个大顶堆或小顶堆(这里采用大顶堆),此时整个序列的最大值就是堆顶的根节点。将它移走(其实就是将其与堆的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个数重新构造成一个大顶堆,这样就能得到整个序列的次大值。如此反复执行,便能得到一个有序序列。

111cabda470657d21fdcc120cca1f232_20190613001742222.gif

图片来源:blog.csdn.net/weixin_4210…

算法步骤

  • 将待排序序列构成一个大顶堆(初始化大顶堆)
  • 取出堆顶元素,将其与未排序序列的最后一个元素交换,并将其余元素重新排列成大顶堆
  • 循环第二步,直到序列有序

代码实现

    void HeapSort(vector<int>& nums)
    {
        for(int i = nums.size()/2-1;i>=0;i--)//初始化大顶堆,i选取的是有孩子结点的元素坐标
        {
            HeapAdjust(nums,i,nums.size()-1);
        }
        for(int i = nums.size()-1;i>=1;i--)
        {
            swap(nums[0],nums[i]);//将堆顶元素与末尾元素交换
            HeapAdjust(nums,0,i-1);//将剩余元素重排为大顶堆
        }
    }
    void HeapAdjust(vector<int>& nums,int start,int end)//重排大顶堆
    {
        int temp = nums[start];//将当前的堆顶元素赋给临时变量
        for(int j = 2*start+1;j<=end;j = j*2+1)//一个节点坐标为i,则其子节点坐标分别为2*i+1、2*i+2
        {
            if(j<end&&nums[j]<nums[j+1])//判断两个子节点,j为较大的那个元素坐标
            {
                j++;
            }
            if(temp>=nums[j])//判断当前堆顶元素的值与子节点的大小,若堆顶元素大则不需要交换直接退出
            {
                break;
            }
            nums[start] = nums[j];//子节点比堆顶元素大,将子节点放到堆顶
            start = j;//用start存放循环结束后堆顶元素应放置的对应下标
        }
        nums[start] = temp;//将堆顶元素放到该放的位置
    }

4、pdqsort

pdqsort是Pattern-defeating quicksort的缩写,是一种新型的排序算法,将随机快速排序的快速平均情况与堆排序的最坏情况快速组合在一起,同时在具有特定模式的输入上实现了线性时间。

pdqsort结合了前三种排序算法的优点:

  • 对于短序列,使用插入排序(小于一定长度,在泛型版本根据测试选定阈值为24)
  • 其他情况,使用快速排序来保证整体性能,其中针对不同长度的序列,可以通过改变pivot的选择策略提高算法性能,选择方法在快速排序的优化部分
  • 当使用快速排序表现不佳时,使用堆排序来保证最坏情况下时间复杂度仍为O(n*logn)

如何得知快速排序表现不佳?何时切换到堆排序?

答:当最终pivot的位置离序列两端很接近时(举例小于length/8)判定其表现不佳,当这种情况出现的次数达到limit时,切换到堆排序

代码实现可参考:github.com/orlp/pdqsor…

5、冒泡排序

冒泡排序的基本思想是:两两比较相邻记录的关键字,如果反序则交换,知道没有反序的记录为止。

代码实现

    void BubbleSort(vector<int>& nums)
    {
        int n = nums.size();
        bool flag = true;//设置标志位,如果循环一次后没有过交换操作说明序列已经有序,可以退出循环
        for(int i = 0;i<n-1&&flag;i++)//循环n-1次,即找到n-1个小泡泡把他放到头部
        {
            flag = false;
            for(int j = n-2;j>=i;j--)//j从后往前循环,两两比较,一步步把小的泡泡往前移
            {
                if(nums[j]>nums[j+1])
                {
                    swap(nums[j],nums[j+1]);
                    flag = true;
                }
            }
        }
    }

6、选择排序

选择排序就是每循环一次就从未排序序列中找到一个最小的记录,然后将它放到有序序列的尾部。

冒泡排序的思想是不断的交换,而选择排序只在确定找到一个最小值的情况下才进行交换,大大减少了交换的次数。

代码实现

void SelectSort(vector<int>& nums)
    {
        int n = nums.size();
        for(int i = 0;i<n-1;i++)//每次遍历未排序数组,依次找到n-1个小数
        {
            int min = i;//将当前元素作为最小元素,并保存其下标
            for(int j = i+1;j<n;j++)//循环未排序数组
            {
                if(nums[min]>nums[j])//如果有小于当前最小值的元素
                {
                    min = j;//将该元素下标作为最小值下标
                }
            }
            if(i!=min)//如果min值改变了,说明找到了一个最小值,如果没改变说明位置i本身就是最小值
            {
                swap(nums[min],nums[i]);//交换
            }
        }
    }

7、希尔排序

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。

代码实现

void SheelSort(vector<int>& nums)//希尔排序采取跳跃分割的策略
    {
        int increment = nums.size();
        while(increment>1)
        {
            increment = increment/3+1;//设置增量,当increment==1时,相当于使用插入排序,因为对于小数组来说插入排序的性能还是不错的
            for(int i = increment;i<nums.size();i++)
            {
                if(nums[i]<nums[i-increment])
                {
                    int temp = nums[i];//临时存放小元素的值,防止大元素后移造成的元素覆盖
                    int j = 0;
                    for(j = i-increment;j>=0&&temp<nums[j];j-=increment)//这一步主要是完成交换操作,可以想象成跳跃式的冒泡,把小元素送到序列前面
                    {
                        nums[j+increment] = nums[j];//把大元素放到后面
                    }
                    nums[j+increment] = temp;//将小元素的值放到序列前面的位置
                }
            }
        }
    }

8、归并排序

归并排序就是将n个记录的序列看成n个有序的子序列,每个子序列长度为1,然后不断两两排序归并,直到得到长度为n的有序序列为止。

归并方法:每次在两个子序列中找到较小的那一个赋值给合并序列(通过指针进行操作)。当一个子序列遍历完成后,将另一个序列中剩下数赋值给合并序列。

64551a547402afb99434101222d408a0_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N1cGVyODI4,size_16,color_FFFFFF,t_70.png

代码实现

void MergeSort(vector<int>& nums)
    {
        int n = nums.size();
        vector<int> temp(n,0);//开辟空间,存放合并的数组
        int k = 1;//子序列的长度
        while(k<n)
        {
            MergePass(nums,temp,k,n);//合并一次
            k = 2*k;//子序列长度加倍
            MergePass(temp,nums,k,n);//再合并一次,主要是将结果返回到nums
            k = 2*k;//子序列长度加倍
        }
    }
    void MergePass(vector<int>& SR,vector<int>& TR,int s,int n)
    {//将两个序列两两合并
        int i = 0;//i为第一个序列的起始位置,i+s为第二个序列的起始位置,要将这两个序列合并
        while(i<n-2*s)//从i开始,还有两个长为s的序列的话才进入循环,将其合并
        {
            Merge(SR,TR,i,i+s,i+2*s);
            i = i+2*s;
        }//剩下的序列有两种情况:1、还剩一个序列2、剩一个长为s的序列和一个不足s的序列
        if(i<n-s)//这里判断剩两个序列的话再合并一次
        {
            Merge(SR,TR,i,i+s,n);
        }
        else//如果只剩一个序列的话就直接将其放到尾部
        {
            for(int j = i;j<n;j++)
            {
                TR[j] = SR[j];
            }
        }
    }
    void Merge(vector<int>& SR,vector<int>& TR,int i,int m,int n)
    {//注意:此时,两个子序列都是有序的,单调递增的
        int j,k;//i为第一个序列的起始位置,m为第二个序列的起始位置
        for(j=m,k=i;i<m&&j<n;k++)//分别从头遍历两个序列,将较小的存放到容器中
        {//如果有一个序列的元素被取完了,就退出循环
            if(SR[i]<SR[j])
            {
                TR[k] = SR[i++];
            }
            else
            {
                TR[k] = SR[j++];
            }
        }
        if(i<m)//第一个子序列的元素没有用完,那这个序列中剩下的元素都是大的,因为小的已经被放到容器里了
        {
            for(int l = 0;l<m-i;l++)//子序列是递增的,直接将剩下的元素放到容器中
            {
                TR[k+l] = SR[i+l];
            }
        }
        if(j<n)//第二个序列的元素没有用完
        {
            for(int l = 0;l<n-j;l++)
            {
                TR[k+l] = SR[j+l];
            }
        }
    }

9、计数排序

计数排序是一种稳定的[排序算法],计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。

算法步骤

  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

84196b2912525e977bd055f8c31fdcf3_20190712143216563.gif

代码实现

    void CountSort(vector<int>& nums)
    {
        int n = nums.size();
        if(n==1)return;
        int max_num = INT_MIN;
        int min_num = INT_MAX;
        for(auto& num:nums)//找出序列中的最大值和最小值
        {
            if(num>max_num)max_num = num;
            if(num<min_num)min_num = num;
        }
        int len = max_num-min_num+1;//
        vector<int> count(len,0);//创建一个容器,其下标对应序列中的元素数值,关键字表示序列中各元素出现的次数
        for(auto& num:nums)//对个元素出现次数计数
        {
            count[num-min_num]++;
        }
        vector<int> temp = count;
        for (int i = 0; i < len-1; i++) //找出各元素排序后的起始位置
        {
		    count[i + 1] += count[i];
            count[i] -= temp[i];
	    }
        count[len-1] -= temp[len-1];
        for(int i = 0;i<n;i++)//按照起始位置摆放元素,对序列进行排序
        {
            temp[count[nums[i]-min_num]++] = nums[i];
        }
        for(int i = 0;i<n;i++)//将答案赋值给原序列
        {
            nums[i] = temp[i];
        }
    }

算法分析

当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。

10、桶排序

桶排序是一种基于计数的排序算法,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素,通过映射函数,将待排序数组中的元素映射到各个对应的桶中,对每个桶中的元素进行排序,最后将非空桶中的元素逐个放入原序列中。

算法步骤:

  • 计算排序所需的桶的数量
  • 通过映射函数,也可以看作是设定桶区间,将未排序数组中的元素放入对应的桶中
  • 对每个桶中的元素进行排序
  • 将桶中的元素放回原序列

代码实现

void BucketSort(vector<int>& nums)
    {
        // 计算最大值与最小值
        int max = INT_MIN;
        int min = INT_MAX;
        for(int i = 0; i < nums.size(); i++)
        {
            max = max>nums[i]?max:nums[i];
            min = min<nums[i]?min:nums[i];
        }
        
        // 计算桶的数量
        int bucketNum = (max - min) / nums.size() + 1;
        vector<vector<int>> bucketArr;
        bucketArr.resize(bucketNum);
        
        // 将每个元素放入桶
        for(int i = 0; i < nums.size(); i++)
        {
            int num = (nums[i] - min) / (nums.size());//映射函数,用于确定元素属于哪个桶,每个桶最多放nums.size()个元素
            bucketArr[num].push_back(nums[i]);
        }
        
        // 对每个桶进行排序
        for(int i = 0; i < bucketArr.size(); i++)
        {
            sort(bucketArr[i].begin(),bucketArr[i].end());//此处也可以用直接插入排序等算法
        }
        
        // 将桶中的元素赋值到原序列
        int index = 0;
        for(int i = 0; i < bucketArr.size(); i++)
        {
            for(int j = 0; j < bucketArr[i].size(); j++)
            {
                nums[index++] = bucketArr[i][j];
            }
        }  
    }

11、基数排序

数排序又称为“桶子法”,从低位开始将待排序的数按照这一位的值放到相应的编号为0~9的桶中。等到低位排完得到一个子序列,再将这个序列按照次低位的大小进入相应的桶中,一直排到最高位为止,数组排序完成。

07ab0c214a8bac217b2985110bff8294_2815742358e3b3d3d606ad46c2b9d716.png 排序数字最多的位数,有多少位,循环多少趟。 第一趟比较个位,完成第一次排序。第二趟比较十位,完成第二次排序,依次比较完

图片来源:blog.csdn.net/qq_49301731…

算法步骤

  • 遍历数组找出最大元素,并计算其位数,用于判断循环次数
  • 新开辟一个二维数组temp,存放临时数组,开辟一个一维数组counts,用于记录元素下标即桶内元素个数
  • 依次比较个元素的个位、十位、百位等位上的数,首先比较个位,依次存放到二维数组中
  • 根据counts中记录的坐标及个数,将二维数组中的元素再放回原数组
  • 循环第三和第四步,直到数组有序

代码实现

void RadixSort(vector<int>& nums) 
{
        // 遍历数组,找出最大值
	int max = INT_MIN;
	for (int i = 0; i < nums.size(); i++) 
        {
		max = max>nums[i]?max:nums[i];
	}
        //计算最大的元素的位数,用于判断循环次数
	int maxLength = 0;
        while(max>0)
        {
            maxLength++;
            max /= 10;
        }
        vector<vector<int>> temp(10,vector<int>(nums.size(),0));
        vector<int> counts(10,0);
        int n = 1;
	for (int i = 0; i < maxLength; i++) 
        {
            for (int j = 0; j < nums.size(); j++) 
            {
		int ys = (nums[j] / n) % 10;
		temp[ys][counts[ys]] = nums[j];
		counts[ys]++;
            }

	// 记录取的数字应该放到位置
            int index = 0;
            for (int k = 0; k < counts.size(); k++) 
            {
		if (counts[k] != 0) 
                {
                    for (int l = 0; l < counts[k]; l++) 
                    {
			nums[index] = temp[k][l];
			index++;
                    }
		counts[k] = 0;
                }
            }
            n *= 10;
	}
}