写在前面:
1.本文中默认排序为升序,降序的原理类似。
2.如果程序直接复制到vs出现无法识别标记的问题,解决方法在这:vs无法识别标记的解决方法
3.本文的算法都是自己用stl实现的,疏漏之处还请指正。
4.在文末对个算法的稳定性进行了详细的讲解!
目录
2.将该序列中小于基准数的数排在其左边,大于基准数的数排在其右边
3.2 交换根节点和倒数第一个数据,现在倒数第一个数据就是最大的。
3.4 重复3.2、3.3的步骤,直到只剩根节点 array[0],即 i=1。
算法概述
1.算法分类
十种常见排序算法分为2大类:
非线性时间比较类:通过比较来决定元素间的相对顺序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。
线性时间非比较类:不通过比较来决定元素间的相对顺序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
2.算法复杂度
3.相关概念
稳定:如果a原本在b前面,而a=b,排序后a仍在b前面
不稳定:如果a原本在b前面,而a=b,排序后a可能会出现在b后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
一、 冒泡排序
1.原理概述
冒泡排序是遍历整个序列,并相邻两元素两两比较,如果反序就交换位置,最终就将最大的数放到序列末尾。遍历第二次就将倒数第二大的数放到了倒数第二的位置,重复该操作,直到遍历n-1次后,整个序列完成排序。
2.算法实现
#include<vector>
#include<string>
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <algorithm>//算法头文件
using namespace std;
class Solution {
public:
void BubbleSort(vector<int>& nums) {
int n = nums.size();
if (nums.empty() || n <= 1) return;
for (int i = n; i > 0; i--) {
for (int j = 1; j < i; j++) {
if (nums[j] < nums[j - 1]) swap(nums[j], nums[j - 1]);
}
}
return;
}
};
int main()
{
vector<int> aa{4,3,5,1,5,8};
Solution sa;
sa.BubbleSort(aa);
for (int i = 0; i < aa.size(); i++)
cout << aa[i] << endl;
system("pause");
return 0;
}
二、选择排序
1.原理概述
遍历整个序列,找到最小元素的位置,然后将其放到序列最开始作为已排序序列。然后再从剩余的序列中找到最小的元素放在已排序序列后面。依次类推直到所有元素排列完毕。
选择排序与冒泡排序的区别是:选择排序遍历完整个序列才交换一次;而冒泡排序是两两比较视情况交换,所以每遍历一个元素都可能交换。
2.算法实现
#include<vector>
#include <iostream>
#include <queue>
#include <algorithm>//算法头文件
using namespace std;
class Solution {
public:
void SelectionSort(vector<int>& nums) {
int n = nums.size();
if (nums.empty() || n <= 1) return;
for (int i = 0; i < n; i++) {
int minpos = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[minpos]) minpos = j;
}
swap(nums[minpos], nums[i]);
}
return;
}
};
int main()
{
vector<int> aa{4,3,5,1,5,8,7 , 25, 6};
Solution sa;
sa.BubbleSort(aa);
for (int i = 0; i < aa.size(); i++)
cout << aa[i] << endl;
system("pause");
return 0;
}
三、快速排序
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-onquerMethod)。它的平均时间复杂度为O(nlogn),最坏时间复杂度为O(n^2)。
快速排序完成的事情就是:选取参照数后,需要将参照数挪到序列中的第k位,并以位置k为分界。实现左边的数小于参照数,右边的数大于参照数。然后递归对左右两个区间做同样的事,就能完成排序。
假设现在要对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。
1.选取基准数
随机选取序列中的一个数作为参照数,一般选第一个,如果选的不是第一个,就将其与第一个交换。比如这里选6作为基准数。
2.将该序列中小于基准数的数排在其左边,大于基准数的数排在其右边
3 1 2 5 4 6 9 7 10 8
具体步骤:
首先要选取哨兵(基准数base),如果是随机选取的,就将其换到第一个位置。定义left指针指向序列首位置,定义right指针指向末尾位置。
1)从数组的right位置向前找,一直找到比(base)小的数,如果找到,将此数赋给left位置,此时right指针指向找到的那个数。
2)从数组的left位置向后找,一直找到比(base)大的数,如果找到,将此数赋给right的位置,此时left指针指向找到的那个数的位置。
3)重复步骤1、2直到left==right。
4)将基准数放在相等的位置上。至此完成一次快排!
5)以刚刚找到的基准数的位置为分界点,对其前后两个新序列重复步骤1-4。直到排序完成。
因为来回覆盖,不会丢掉数。(首先将基数放在第一个位置上。从右方开始,小的往前面覆盖,也就是放在基数位置上。从左方开始大的往后面覆盖,也就是放在刚刚最小的那个数上面)
3.算法实现
#include<vector>
#include<string>
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <algorithm>//算法头文件
using namespace std;
class Solution {
public:
void QuickSort(vector<int>& points) {
if (points.empty()) return ;
int len = points.size();
int left = 0, right = len - 1;//序列的左右边界
Sort(points, left, right);
}
void Sort(vector<int>& points, int left, int right) {
if (points.empty()) return;
if (left >= right) return;
//防止有序队列导致快速排序效率降低
int len = right - left;
int flag = points[left], i = left, j = right;
while(i < j) {
while(points[j] >= flag && i < j) --j;//一直向前遍历 直到位置j上的数小于哨兵
if (i < j) points[i] = points[j];//将找到的小的那个数覆盖到前面
while (points[i] <= flag && i < j) ++i;//一直向后遍历 直到位置i上的数大于哨兵
if (i < j) points[j] = points[i];//将找到的大的那个数 覆盖到后面
}
points[i] = flag;//因为此时位置i左边的数都小于flag 右边的数都大于flag 位置i上的数是个重复的数
Sort(points, left, i - 1);//递归左边序列
Sort(points, i + 1, right);//递归右边序列
}
};
int main()
{
vector<int> aa{4,3,5,1,5,8,7 , 25, 6};
Solution sa;
sa.QuickSort(aa);
for (int i = 0; i < aa.size(); i++)
cout << aa[i] << endl;
system("pause");
return 0;
}
四、插入排序
1.原理概述
直接插入排序,也简称为插入排序。基本思想是:
假设左边i个元素已经排好序,从i开始,从左向右开始遍历,将遍历到的元素放在已排序列中第一个小于该元素的元素后面。
直接插入排序对于最坏的情况(严格递减的序列),需要比较和移位的次数为n(n-1)/2;对于最好的情况(严格递增的序列),需要比较的次数为n-1,需要移位的次数为0。
直接插入排序法对于基本有序的序列会有更好的性能,这一特性给了它进一步优化的可能性(希尔排序)。
2.示例
使用直接插入排序法对序列89 45 54 29 90 34 68进行升序排序。
89 45 54 29 90 34 68
45 89 54 29 90 34 68
45 54 89 29 90 34 68
29 45 54 89 90 34 68
29 45 54 89 90 34 68
29 34 45 54 89 90 68
29 34 45 54 68 89 90
3.算法实现
#include<vector>
#include<string>
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <algorithm>//算法头文件
using namespace std;
class Solution {
public:
void InsertSort(vector<int>& nums) {
int n = nums.size();
if (n <= 1) return;
for (int i = 1; i < n; i++) {
for (int j = i; j > 0; j--) {
if (nums[j] < nums[j - 1]) swap(nums[j], nums[j - 1]);
else break;
}
}
return;
}
};
int main()
{
vector<int> aa{4,3,5,1,5,8,7 , 25, 6};
Solution sa;
sa.InsertSort(aa);
for (int i = 0; i < aa.size(); i++)
cout << aa[i] << endl;
system("pause");
return 0;
}
五、希尔排序
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,是在简单插入排序基础上改进的一个高效版本,也称为缩小增量排序。该算法是第一批冲破O(n2)的算法之一。利用了插入排序的最佳时间代价特性,它试图将待排序序列变成基本有序的,然后再用插入排序来完成排序工作。
(这里再提一下插入排序,直接插入排序是从前往后一次遍历每一个并将其放在前面第一个小于它的元素后面)
1.原理概述
由于直接插入排序需要一步一步的对元素进行比较、移动、插入。希尔排序在此基础上采用跳跃分组的策略,通过增量(跳跃的量化)将序列划分为若干组,然后分组进行插入排序,接着缩小增量,继续按组进行插入排序操作,直至增量为1。采用这个策略的希尔排序使整个序列在初始阶段从宏观上看就基本有序,小的基本在前面,大的基本在后面。然后缩小增量至增量为1时,多数情况下只需要微调就可以了,不涉及到大量的数据移动。
2.排序步骤
首先选择增量gap=lengh/2,缩小增量至gap=gap/2。增量的可以用增量序列来表示,如{n/2,(n/2)/2……1}。
这个增量序列也称为希尔增量,其实这个增量序列不是最优的。
算法实现关键是在序列中每隔gap取一个构成一组,然后对每一组进行插入排序操作。
3.算法实现
#include<vector>
#include<string>
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <algorithm>//算法头文件
using namespace std;
class Solution {
public:
const int INCRGAP = 3;//增量初始化
void ShellSort(vector<int> &nums)
{
int len = nums.size();
unsigned gap = len / INCRGAP + 1; // 步长初始化,注意如果当len<INCRGAP时,gap为0,所以为了保证进入循环,gap至少为1!!!
while (gap) // while gap>=1
{
for (unsigned i = gap; i < len; ++i) // 分组,在每个子序列中进行插入排序
{
unsigned j = i;
while (j >= gap && nums[j] < nums[j - gap])//直接插入排序
{
swap(nums[j], nums[j - gap]);
j -= gap;
}
}
gap = gap / INCRGAP;
}
}
};
int main()
{
vector<int> aa{ 2, 1, 4, 3, 11, 6, 5, 7, 8, 10, 15 };
int K = 8766;
Solution sa;
sa.ShellSort(aa);
for (int i = 0; i < aa.size(); i++)
cout << aa[i] << endl;
system("pause");
return 0;
}
六、归并排序
1.原理概述
归并排序是利用归并的思想实现的排序方法。该方法采用分治策略(分治法将问题分解为一些小问题,然后递归求解;而治阶段将分段得到的答案合并在一起)。
2.示例
这种结构类似于完全二叉树,归并排序需要使用递归或者迭代的方法实现。
分阶段就是递归拆分子序列,递归深度为log2n。
治阶段需要将两个已经有序的子序列相互比较再合并。
3.算法实现
#include<vector>
#include<string>
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <algorithm>//算法头文件
using namespace std;
class Solution {
public:
void MergeSort(vector<int>& nums) {
int n = nums.size();
if (n <= 1) return;
int left = 0, right = n - 1;
Sort(nums, left, right);//开始归并排序
}
void Sort(vector<int> &nums, int left, int right) {
if (left >= right) return;
int mid = (left + right) / 2;//将序列分为2部分
Sort(nums, left, mid);//处理左半部分序列
Sort(nums, mid + 1, right);//处理右半部分序列
/***下面是对序列进行排序合并处理,可以单独用一个函数完成***/
vector<int> temp;//排序的临时容器
int i = left, j = mid + 1;
while (i <= mid && j <= right) {//对两个序列进行排序并合并
if (nums[i] <= nums[j])
temp.push_back(move(nums[i++]));
else
temp.push_back(move(nums[j++]));
}
while (i <= mid) temp.push_back(move(nums[i++]));
while (j <= right) temp.push_back(move(nums[j++]));
for (int k = 0; k < right - left + 1; k++)
{
nums[left + k] = temp[k];
}
}
};
int main()
{
vector<int> aa{4,10,5,1,5,8,7 , 9, 6};
Solution sa;
sa.MergeSort(aa);
for (int i = 0; i < aa.size(); i++)
cout << aa[i] << endl;
system("pause");
return 0;
}
七、堆排序
1.原理概述
堆排序是利用堆的性质来进行排序的。
2.堆:
堆实际上是一颗完全二叉树,满足两个性质:
1.堆的父节点都大(小)于其子节点;2.堆的每个子树也是一个堆。
堆分为两类:
最大堆:堆的每个父节点都大于其孩子节点;
最小堆:堆的每个父节点都小于其孩子节点。
堆的存储:第i个节点的父节点下标为(i-1)/2。对应的其左右孩子节点下标为2*i+1和2*i+2.
3.实现原理
要实现从小到大的排序,就要建立大顶堆,即父节点比子节点都要大。
3.1 初始化数组,创建大顶堆。
大顶堆的创建从下往上比较,不能直接用无序数组从根节点比较,否则有的不符合大顶堆的定义。
3.2 交换根节点和倒数第一个数据,现在倒数第一个数据就是最大的。
3.3 重新建立大顶堆。
因为只有 array[0] 改变,其它都符合大顶堆的定义,所以可以根节点 array[0] 重新建立。
3.4 重复3.2、3.3的步骤,直到只剩根节点 array[0],即 i=1。
4.排序实例
原始数据:array[]={49,38,65,97,76,13,27,49,10}
要升序排序,就构建最大堆。
原始堆排序
创建大顶堆
从倒数第二行往上比较父节点和子节点,把大的往上移动,小的向下移,直到符合大顶堆定义。
交换根节点和最后一个节点
重新创建大顶堆
接下来就一直循环即可得到堆排序结果
5.算法实现
#include <iostream>
#include<vector>
#include<string>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <algorithm>//算法头文件
using namespace std;
class Solution {
public:
//堆排序
/*
大顶堆sort之后,数组为从小到大排序
*/
//====排序=====
void HeapSort(vector<int> &nums)
{
int len = nums.size();
MakeHeap(nums);
for (int i = len - 1; i >= 0; --i)
{
swap(nums[i], nums[0]);
AdjustHeap(nums, 0, i);
}
}
//====调整=====
void AdjustHeap(vector<int> &nums, int node, int len) //----node为需要调整的结点编号,从0开始编号;len为堆长度
{
int index = node;
int child = 2 * index + 1; //左孩子,第一个节点编号为0
while (child < len)
{
//右子树
if (child + 1 < len && nums[child] < nums[child + 1])
{
child++;
}
if (nums[index] >= nums[child]) break;
swap(nums[index], nums[child]);
index = child;
child = 2 * index + 1;
}
}
//====建堆=====
void MakeHeap(vector<int> &nums)
{
int len = nums.size();
for (int i = len / 2; i >= 0; --i)
{
AdjustHeap(nums, i, len);
}
}
};
int main()
{
vector<int> aa{ 8,5,0,3,7,1,2 };
//int K;
vector<int> sa;
Solution ss;
ss.HeapSort(aa);
for (int i = 0; i < aa.size(); i++)
cout << aa[i] << endl;
system("pause");
return 0;
}
八、计数排序
1.原理概述
计数排序不是基于比较的排序算法,其核心是将输入的数据值转化为键存储在额外的空间。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
2.排序步骤
找出待排序的序列中的最大值和最小值;
统计序列中每个值为i的元素出现的次数,存入序列C的第i项;
对所有的计数累加(对C按项求和);
反向填充目标序列,将每个元素i放在新序列的第C(i)项,每放一个元素将C(i)减一。
计数排序是一种不需要比较的排序,比任何比较的排序都要快。
适用于数组中的值不是特别大的情况,因为需要用空间换时间,所以当数组中的值特别大的时候,空间开销会超级大。从代码中可以明显看出来。
此外,计数排序只能用于正数排序(个人认为负数排序也是可以的)。
3.算法实现
#include <iostream>
#include<vector>
#include<string>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <algorithm>//算法头文件
using namespace std;
class Solution {
public:
void CounterSort(vector<int> &nums) {
if (nums.empty()) return;
int len = nums.size();
int max_num = *(max_element(nums.begin(), nums.end()));
vector<int> tmp(max_num+1, 0);//根据最大数构建一个数组
for (int i = 0; i < len; i++) {
tmp[nums[i]]++;
}
vector<int>().swap(nums);
for (int i = 0; i < tmp.size(); i++) {
while (tmp[i] != 0) {
nums.push_back(i);
tmp[i]--;
}
}
}
};
int main()
{
vector<int> aa{45, 1, 9, 18,5,0,23,47,15,2 };
//int K;
vector<int> sa;
Solution ss;
ss.CounterSort(aa);
for (int i = 0; i < aa.size(); i++)
cout << aa[i] << endl;
system("pause");
return 0;
}
九、基数排序(也可以叫低位到高位的基数排序)
1.原理概述
基数排序与前面大部分排序方法都不相同,它不需要比较关键字的大小。
它根据关键字中各位的值,通过对排序的N个元素进行若干次“分配”与“收集”来实现排序。
其原理就是从个位开始排序,在低位数满足排序的情况下,低位对整体排序的作用已经完成,只需要高位也满足排序规则即可。所以从个位开始逐位放入桶中再按桶号取出即可。
LSD(低位到高位的排序)
下面通过一个具体的实例来展示基数排序。设置初始序列:
{50, 123, 543, 187, 49, 30, 0, 2, 11, 100}。
每个位置的数字其各位上的基数都是0~9来表示,所以可以将0~9视为10个桶。
先根据序列的个位数的数字来进行分类,将其分到指定的桶中。例如:
分类后,我们再从各个桶中,将这些数按照从编号0到编号9的顺序依次将所有数取出来。
这时,得到的序列就是个位数上呈递增趋势的序列。
原始序列: {50, 123, 543, 187, 49, 30, 0, 2, 11, 100}。
按照个位数排序: {50, 30, 0, 100, 11, 2, 123, 543, 187, 49}。
再对十位进行相同的操作,然后依次将数取出,
得到按照十位数排序:{0, 2,100,11,123,30,543,49,50,187}。
再对百位进行相同的操作,然后将数取出。
得到按照百位排序:{0, 2,11,30,49,50,100,123,187,543}
最终排序完成。
2.算法实现
LSD(低位到高位的排序)
#include <iostream>
#include<vector>
#include<string>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <algorithm>//算法头文件
using namespace std;
class Solution {
public:
void RadixSort_LSD(vector<int> &nums) {
if (nums.empty()) return;
int len = nums.size();
//得到最高位到哪
int max_num = *(max_element(nums.begin(), nums.end()));
int min_num = *(min_element(nums.begin(), nums.end()));//防止有负数
int high1 = 0, high2 = 0, high = 0;
while (max_num != 0) {
high1++;
max_num /= 10;
}
while (min_num != 0) {
high2++;
min_num /= 10;
}
high = max(high1, high2);
vector<vector<int>> Bucket(10, vector<int>());
vector<int>tmp;
tmp = nums;
for (int i = 1; i <= high; i++) {
for (int j = 0; j < tmp.size(); j++) {
Bucket[abs(tmp[j] % 10)].push_back(nums[j]);
}
//重新组成临时排序数组
vector<int>().swap(nums);
vector<int>().swap(tmp);
for (int m = 0; m < 10; m++) {
for (int n = 0; n < Bucket[m].size(); n++) {
nums.push_back(Bucket[m][n]);//每处理一次 去掉最低位
tmp.push_back(Bucket[m][n]/10);
}
vector<int>().swap(Bucket[m]);
}
}
//再处理数组中的负数
deque<int> rev;
for (int i = 0; i < len; i++) {
if (nums[i] < 0) rev.push_front(nums[i]);
else rev.push_back(nums[i]);
}
vector<int>().swap(nums);
for (auto x : rev) {
nums.push_back(x);
}
}
};
int main()
{
vector<int> aa{45, -1, -995, 18,5,0,-23,47,15,2 };
//int K;
vector<int> sa;
Solution ss;
ss.RadixSort_LSD(aa);
for (int i = 0; i < aa.size(); i++)
cout << aa[i] << endl;
system("pause");
return 0;
}
十、桶排序
1.原理概述
桶排序是基数排序和计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
2.排序实例
待排序序列
170, 45, 75, 90, 2, 24, 802, 66
我们看到,这里面的最大的数是3位数。所以说我们开始从百位对这些数进行分组
0: 045, 075, 090,002, 024, 066
1: 170
2-7: 空
8: 802
9: 空
从这里开始就和LSD基数排序有差别了。在LSD基数排序中,每次分好组以后开始对桶中的数据进行收集。然后根据下一位,对收集后的序列进行分组。在这里不会对桶中的数据进行收集。我们要做的是检测每个桶中的数据。当桶中的元素个数多于1个的时候,要对这个桶递归进行下一位的分组。
在这个例子中,我们要对0桶中的所有元素根据十位上的数字进行分组
0: 002
1: 空
2: 024
3: 空
4: 045
5: 空
6: 066
7: 075
8: 空
9: 090
按照上面所说,我们应该再递归的对每个桶中的元素根据个位上的数进行分组。但是我们发现,现在在每个桶中的元素的个数都是小于等于1的。因此,到这一步我们就开始回退了。也就是说我们开始收集桶中的数据。收集完成以后,回退到上一层。此时按照百位进行分组的桶变成了如下的形式
0: 002, 024, 045,066, 075, 090
1: 170
2-7: 空
8: 802
9: 空
然后我们在对这个桶中的数据进行收集。收集起来以后序列如下
2, 24, 45, 66, 75, 90, 170, 802
整个桶排序就是按照上面的过程进行的。
其实怎么分桶也是由映射函数决定的,下面程序就用了分段的方法。
3.算法实现
#include <iostream>
#include<vector>
#include<string>
#include <unordered_map>
#include <unordered_set>
#include <queue>
#include <algorithm>//算法头文件
using namespace std;
class Insert {
public:
void InsertSort(vector<int>& nums) {
int n = nums.size();
if (n <= 1) return;
for (int i = 1; i < n; i++) {
for (int j = i; j > 0; j--) {
if (nums[j] < nums[j - 1]) swap(nums[j], nums[j - 1]);
else break;
}
}
return;
}
};
class Solution {
public:
void bucketSort(vector<int> &nums) {
if (nums.size() ==0) {
return ;
}
int minValue = *(min_element(nums.begin(), nums.end()));
int maxValue = *(max_element(nums.begin(), nums.end()));
// 桶的初始化
int DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5
int bucketCount = ((maxValue - minValue) / DEFAULT_BUCKET_SIZE) + 1;
vector<vector<int>> buckets(bucketCount,vector<int>());
// 利用映射函数将数据分配到各个桶中
for (int i = 0; i < nums.size(); i++) {
buckets[((nums[i] - minValue) / DEFAULT_BUCKET_SIZE)].push_back(nums[i]);//其实这里已经分段存入桶中了
}
Insert ins;
vector<int>().swap(nums);
for (int i = 0; i < buckets.size(); i++) {
ins.InsertSort(buckets[i]); // 对每个桶进行排序,这里使用了插入排序
for (int j = 0; j < buckets[i].size(); j++) {
nums.push_back(buckets[i][j]);
}
}
}
};
int main()
{
vector<int> aa{45, 1, 9, 18,5,0,23,47,15,2 };
//int K;
vector<int> sa;
Solution ss;
ss.bucketSort(aa);
for (int i = 0; i < aa.size(); i++)
cout << aa[i] << endl;
system("pause");
return 0;
}
十一、经典算法稳定性分析
参考链接:https://www.cnblogs.com/Leophen/p/11397731.html
一、不稳定排序算法有哪些
1、堆排序
2、希尔排序
3、快速排序
4、选择排序
口诀:一堆(堆)希尔(希尔)快(快速)选(选择)
二、常见排序算法稳定性分析
1、堆排序稳定性分析
我们知道堆的结构是节点i的孩子为 2*i 和 2*i+1 节点,大顶堆要求父节点大于等于其 2个子节点,小顶堆要求父节点小于等于其 2 个子节点。
在一个长为 n 的序列,堆排序的过程是从第 n/2 开始和其子节点共 3 个值选择最大(大顶堆)或者最小(小顶堆),这 3 个元素之间的选择当然不会破坏稳定性。
但当为 n/2-1, n/2-2, ...1 这些个父节点选择元素时,就会破坏稳定性。
有可能第 n/2 个父节点交换把后面一个元素交换过去了,而第 n/2-1 个父节点把后面一个相同的元素没有交换,那么这 2 个相同的元素之间的稳定性就被破坏了。
所以,堆排序不是稳定的排序算法。
2、希尔排序
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;
当元素基本有序时,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比 O(N^2) 好一些。
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性
就会被打乱。
所以 shell 排序是不稳定的排序算法。
3、快速排序
快速排序有两个方向,左边的 i 下标一直往右走(当条件 a[i] <= a[center_index] 时),其中 center_index 是中枢元素的数组下标,一般取为数组第 0 个元素。而右边的 j 下标一直往左走(当 a[ j] > a[center_index] 时)。
如果 i 和 j 都走不动了,i <= j, 交换 a[i] 和 a[ j],重复上面的过程,直到 i>j。交换 a[ j]和 a[center_index],完成一趟快速排序。
在中枢元素和 a[ j] 交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11 现在中枢元素 5 和 3(第 5 个元素,下标从 1 开始计)交换就会把元素 3 的稳定性打
乱。
所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和 a[ j] 交换的时刻。
4、选择排序
选择排序即是给每个位置选择待排序元素中当前最小的元素。比如给第一个位置选择最小的,在剩余元素里面给第二个位置选择次小的,依次类推,直到第 n-1 个元素,第 n 个元素不用选择了,因为只剩下它一个最大的元素了。
那么,在一趟选择时,如果当前锁定元素比后面一个元素大,而后面较小的那个元素又出现在一个与当前锁定元素相等的元素后面,那么交换后位置顺序显然改变了。
举个例子:序列5 8 5 2 9, 我们知道第一趟选择第 1 个元素 5 会与 2 进行交换,那么原序列中两个5的相对先后顺序也就被破坏了。
所以选择排序不是一个稳定的排序算法。
5、冒泡排序
冒泡排序就是把小的元素往前调(或者把大的元素往后调)。注意是相邻的两个元素进行比较,而且是否需要交换也发生在这两个元素之间。
所以,如果两个元素相等,我想你是不会再无聊地把它们俩再交换一下。
如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个元素相邻起来,最终也不会交换它俩的位置,所以相同元素经过排序后顺序并没有改变。
所以冒泡排序是一种稳定排序算法。
6、插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有 1 个元素,也就是第一个元素(默认它有序)。
比较是从有序序列的末尾开始,也就是把待插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面。
否则一直往前找直到找到它该插入的位置。如果遇见一个与插入元素相等的,那么把待插入的元素放在相等元素的后面。
所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序仍是排好序后的顺序。
所以插入排序是稳定的。
7、归并排序
归并排序是把序列递归地分成短序列,递归出口是短序列只有 1 个元素(认为直接有序)或者 2 个序列(1 次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。
可以发现,在 1 个或 2 个元素时,1 个元素不会交换,2 个元素如果大小相等也没有人故意交换,这不会破坏稳定性。
那么,在短的有序序列合并的过程中,稳定是是否受到破坏?
没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。
所以,归并排序也是稳定的排序算法。
8、基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。
有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序结果就是高优先级高的在前,高优先级相同的情况下低优先级高的在前。
基数排序基于分别排序,分别收集。
所以其是稳定的排序算法。