文章目录
1 综述
1.1 稳定性
比如A和B的数值相同,但未排序的时候A在B的前面,此时进行排序:
若排序后A依然在B前,则为稳定排序;
若排序后B跑到了A前,则为不稳定排序;
只要有一组发生了这样的情况,就是不稳定的排序了。
稳定的排序算法:归桶插计冒(简记为:鬼瞳插鸡毛??哈哈)
1.2 复杂度
时间复杂度:排序的两种操作:尽可能少的比较次数和尽可能少的移动次数;
空间复杂度:其他存储空间,临时空间等;
算法复杂性:冒泡、选择、插入,这三种属于简单算法;改进算法分别有希尔、堆、归并、快排。
低端的排序算法(不能突破O(n²) 的算法):
排序算法 | 最好时间复杂度 | 最差时间复杂度 | 平均时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n²) | O(n²) | O(1) | 稳定 |
插入排序 | O(n) | O(n²) | O(n²) | O(2) | 稳定 |
选择排序 | O(n²) | O(n²) | O(n²) | O(2) | 不稳定 |
进化的排序算法 O(n²) ~ O(nlogn) :
排序算法 | 最好时间复杂度 | 最差时间复杂度 | 平均时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
带终止标志的改良冒泡排序 | O(n) | O(n²) | O(n²) | O(2) | 稳定 |
带跳跃数的插入排序:希尔排序 | O(n1.3) | O(n²) | O(nlogn) | O(2) | 不稳定 |
带备忘录的选择排序:堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
高端的排序算法(用空间换时间的O(nlogn)) :
排序算法 | 最好时间复杂度 | 最差时间复杂度 | 平均时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(n²) | O(nlogn) | O(logn) | 不稳定 |
2 具体排序算法
2.1 冒泡排序
1、思想:两两比较相邻记录,反序则交换,将后面的小值一点点浮到前面达到有序(冒泡模型);
2、相邻:每次比较的两个都是相邻的;
3、后面的小值:应从后向前遍历;
4、复杂度分析:无终止条件标志,故O(n²),空间复杂度在交换操作处。
2.2 插入排序
1、思想:每次处理一个数字,顺序不对时,将前面有序的比他大的数字都往后挪一下,然后将它插入到应在的位置(扑克牌抓牌模型);
2、顺序不对时:当他的值比前一个值大的时候,才进入第二层循环;
3、前面有序:每次处理完一个数字时。此数字及前面的数字都是有序的;
4、往后挪:第二层循环是部分有序数字的整体向后走一下;
5、复杂度分析:移动和比较次数都相对较少,比前面两种算法时间复杂度低一些。空间复杂度在交换操作处有O(1),存待插入的最小值处还有O(1)。
2.3 选择排序
1、思想:每次找到最小的值,将其该次循环放在最前面的位置(群2炮灰模型);
2、每次找到最小:需要一个额外的空间来存储最小值,当然存最小值的下标更好;
3、放在最前面:最小的下标和当次循环最前面的下标交换。
4、复杂度分析:比较操作无论是否有序都是O(n²),但交换操作其实很少,虽然看起来最优时间复杂度很不好,但一般来说性能还好于冒泡排序;空间复杂度在交换操作处有O(1),存最小值下标处还有O(1)。
2.4 进化的冒泡排序:增加终止条件
1、思想:两两比较相邻记录,反序则交换,将后面的小值一点点浮到前面,直到再无反序。
2、比起冒泡排序,多了一句“直到再无反序”:即需要有个终止条件标志。
3、此终止标志在已经有序时生效,跳出排序过程。
4、复杂度分析:有终止条件标志,故最好情况即已经有序,为O(n),空间复杂度在交换操作处有O(1),一个判断标志O(1)。
2.5 进化的插入排序:希尔排序
1、思想:跳跃式的插入排序,找到前面几个的那个数字,若大则与它交换,做到基本有序,多次插入排序后最终有序。
2、跳跃式:不再是一步步的挪,将插入排序的循环中1变成一个称为增量的变量;
3、前面几个:就是增量,增量是人为设计的,影响性能;每次循环都要减小增量,到1时则已经有序;
4、基本有序:每次循环一次都能做到小的基本前面,大的基本都在后面
5、多次插入排序:增量的减小过程导致,在基本的插入排序外面还需要一层do-while语句。
6、复杂度分析:突破了O(n²)的限制,一般情况为O(n1.5),空间复杂度在交换操作处有O(1),一个判断标志O(1)。
2.6 进化的选择排序:堆排序
1、思想:为了避免前一次排序进行过的比较,借助大顶堆(也叫最大堆),通过虚拟的二叉树结构(本质还是个数组)进行选择排序。将其按广度优先遍历存入数组构成堆数组,随后只需要将堆顶与堆数组末尾元素交换位置,排除掉末尾元素完成即可排序。
2、虚拟的二叉树:其实只是个数组,但假想成二叉树的结构,只利用了完全二叉树种节点n的孩子结点是2n和2n+1这一条性质进行的;
3、堆数组:每次把最大值放在数组最前面的位置,看似有堆的性质;
4、堆顶与堆数组末尾元素:每次把堆顶也就是最大值与数组的最后一个元素交换位置,即最大值放在了最后;
5、排除掉末尾元素:最大值在最后了,下一轮找次最大值放在倒数第二个位置即可,因此完成一轮,最后的下标-1;
6、复杂度分析:第一次构建堆的度咋读为O(n),后面重建堆需要O(logn)的时间。故时间复杂度O(nlogn)。空间上只需要一个暂存最小下标数值的内存,故O(1)。
2.7 归并排序
归并排序思想:将序列分成长度为2或1的小序列,两两归并,继续两两归并,直到有序。(倒二叉树模型)
递归调用Msort分组,分组后调用Merge合并。
2.8 快速排序
快速排序思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小。然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
递归调用Qsort处理大小两部分序列,其中调用Partition找到具体下标位置。
3 其他非比较排序方式
3.1 计数排序
准备1个数组,把利用元素的实际值作为key,确定它们在输出数组中的位置,遍历新数组即可。
3.2 基数排序
准备10个数组。先把所有数据的个位扔进去,然后遍历这10个数组的每个值,第一轮排序结束;然后遍历十位,以此类推。
3.3 桶排序
把数组的区间分成若干个桶,然后分别排序。
4 参考代码
#include"sort.h"
/
// 冒泡排序 //
/
void BubbleSort0(vector<int>& L)//虚假的冒泡排序
{
for (int i = 0; i < L.size(); i++)
{
for (int j = 0; j < i; j++)
{
if (L[j] > L[i])//每一个关键字都与后面的每个关键字比较,并不是比较的相邻记录
{
swap(L[j], L[i]);
}
}
}
}
void BubbleSort1(vector<int>& L)//正宗的冒泡排序
{
for (int i = 0; i < L.size(); i++)
{
for (int j = L.size() - 1; j > i; j--)//正宗的原因:第二层循环的j是从后往前循环
{
if (L[j - 1] > L[j])//较小的数字从底下慢慢浮到上面
{
swap(L[j - 1], L[j]);
}
}
}
}
void BubbleSort2(vector<int>& L)//进一步优化的冒泡排序
{
bool FLAG = true;//改进1:初始化一个标记
for (int i = 0; i < L.size() && FLAG; i++)//改进2:如果FALG为false,则提前跳出循环,避免有序时做无意义的循环判断
{
FLAG = false;//改进3:此标记在新一层循环初始化为false
for (int j = L.size() - 1; j > i; j--)
{
if (L[j - 1] > L[j])
{
swap(L[j - 1], L[j]);
FLAG = true;//改进4:如果有数据交换,才令flag为true,判断是否有数据交换
}
}
}
}
/
// 选择排序 //
/
void SelectSort(vector<int>& L)//选择排序
{
int min;//min是一个下标,用下标可以避免初始化的问题。选择排序的要点:每次找到最小值的下标
for (int i = 0; i < L.size(); i++)
{
min = i;//i是每次最小值应当放的位置
for (int j = i + 1; j < L.size(); j++)//j从i+1开始即可,不必从i开始
{
if (L[min] > L[j])//如果值更小,则更新最小值的下标
{
min = j;
}
}
if (i != min)//这个判断很鸡肋?用一个判断操作来减少多余的赋值操作
{
swap(L[i], L[min]);
}
}
}
/
// 插入排序 //
/
void InsertSort(vector<int>& L)//插入排序
{
for (int i = 1; i < L.size() ; i++)
{
if (L[i] < L[i - 1])
{
int j;
int insert = L[i];
for (j = i - 1; L[j] > insert && j >= 0; j--)
{
L[j + 1] = L[j];
}
L[j + 1] = insert;
}
}
}
/
// 希尔排序 //
/
void ShellSort(vector<int>& L)//希尔排序:改良的插入排序
{
int increment = L.size();//增量序列:大量数据进行分组
do
{
increment = increment / 3 + 1;//增量序列:不是一步一步的挪动,而是跳跃式的向前插入。这里的3如何确定是个数学难题
for (int i = increment ; i < L.size(); i++)//这里的for就是个插入排序,把所有的1变成了increment而已
{
if (L[i] < L[i - increment])
{
int j;
int insert = L[i];//插入的变成了有序增量子表
for (j = i - increment; L[j] > insert && j >= 0; j -= increment)
{
L[j + increment] = L[j];
}
L[j + increment] = insert;
}
}
} while (increment > 1);//当增量大于1的时候就继续进行
}
/
/// 堆排序 ///
/
void HeapAdjust(vector<int>& L, int s, int m);//声明需要用的函数HeapAdjust
/*假设下标s到m的关键字除了s下标的位置都满足了堆的定义,调整L[s]的关键字,使其成为大顶堆*/
void HeapSort(vector<int>& L)//堆排序:改良的插入排序。构成一个大顶堆,随后把最大的值放在最后,然后排除掉最后一个元素递归
{
for (int i = (L.size() - 1)/2; i >= 0; i--)
{
HeapAdjust(L,i,L.size() - 1);//从数组的一半开始(因为要从二叉树的非叶子结点找),循环把L构成一个大顶堆(结点调整过程)
}
for (int i = L.size() - 1; i > 0; i--)//不用≥0因为最后一步可以不操作
{
swap(L[0], L[i]);//将最大堆的顶部放进数组最后一个位置,同时堆顶设为数组最后一个位置的值
HeapAdjust(L,0, i - 1);//不管最后一个位置,重新调整成大顶堆,继续处理前面的位置
}
}
void HeapAdjust(vector<int>& L, int s, int m)//s是下标最小的值,m是下标最大的值,从s到m就是下标范围
{
int temp = L[s];//temp寄存下标最小下标的数值
for (int j = 2 * s ; j <= m; j *= 2)//这里是在找s的孩子结点(完全二叉树的结点n的孩子结点是2n和2n+1),沿关键字较大的孩子结点向下筛选
{
if (j < m && L[j] < L[j + 1])//j<m是为了不超范围,L[j]是左孩子的值,L[j+1]是右孩子的值,j保存着两个孩子大的那个值的下标
{
++j;
}
if (temp >= L[j])//如果当前结点比两个孩子都大,就跳出循环
{
break;
}
L[s] = L[j];//如果不是当前结点比两个孩子都大,把数值换一下,
s = j;//如果不是当前结点比两个孩子都大,同时也把下标换一下,
}
L[s] = temp;//把最小下标的数值存为最大的值
}
//
/// 归并排序 /
/
void Msort(vector<int>& data, vector<int>& copy, int start, int end);//子数组排序过程
void Merge(vector<int>& data, vector<int>& copy, int start, int length, int end);//两个数组合并过程
void MergeSort(vector<int>& L)
{
vector<int> copy;//需要整体复制一下全部的数组,这里开辟空间
for (int i = 0; i < L.size(); i++)
{
copy.push_back(L[i]);
}
Msort(L, copy, 0, L.size() - 1);//把数组封装成一个四参数的函数,用于后面的递归(0是开始位置,L.size()-1是结束位置)
}
void Msort(vector<int>& data, vector<int>& copy, int start, int end)
{
if (start == end)
{
copy[start] = data[start];//copy:在Msort阶段寄存归并后的数组,在Merge阶段合并存回原数组data中
return;
}
int length = (end - start) / 2; //将数组平分成两部分,得到每部分的长度
Msort(copy, data, start, start + length); //将前一半数组排序,并存在copy[start ... start+length]中
Msort(copy, data, start + length + 1, end); //将后一半数组排序,并存在copy[start+length+1 ... end]中
Merge(data, copy, start, length, end); //将前后两个排序数组合并,存在data[start+length+1 ... end]中
}
void Merge(vector<int>& data, vector<int>& copy, int start, int length, int end)//两个数组合并过程
{
int i = start + length; //原数组前一半的最后一个索引
int j = end; //原数组后一半的最后一个索引
int newIndex = end; //data数组最后一个索引
while (i >= start && j >= start + length + 1)//归并过程:从后向前
{
if (copy[i] > copy[j])
{
data[newIndex] = copy[i];
newIndex--;
i--;
}
else
{
data[newIndex] = copy[j];
newIndex--;
j--;
}
}
while (i >= start)//处理没有归并进去的剩余部分(最小的一些数字)
{
data[newIndex] = copy[i];
newIndex--;
i--;
}
while (j >= start + length + 1)//处理没有归并进去的剩余部分(最小的一些数字)
{
data[newIndex] = copy[j];
newIndex--;
j--;
}
}
//
/// 快速排序 /
/
void Qsort(vector<int>& data, int length, int start, int end);
int Partition(vector<int>& data, int length, int start, int end);//把比这个数字大的都放在这个数字下标的后面,比这个数字小的都放在这个数字下标的前面,返回这个下标。
void QuickSort(vector<int>& L)
{
Qsort(L, L.size(), 0, L.size() - 1);//把数组封装成一个四参数的函数,用于后面的递归(0是开始位置,L.size()-1是结束位置)
}
void Qsort(vector<int>& data, int length, int start, int end)
{
if (start == end)//终止条件:只剩一个数字
return;
int index = Partition(data, length, start, end);//把比这个数字大的都放在这个数字下标的后面,比这个数字小的都放在这个数字下标的前面,返回这个下标。
if (index > start)
Qsort(data, length, start, index - 1);//递归:在这堆数字前面的小数子数组,进行递归切分
if (index < end)
Qsort(data, length, index + 1, end);//递归:在这堆数字前面的大数子数组,进行递归切分
}
int Partition(vector<int>& data, int length, int start, int end)//start是起始位置下标,end是终止位置后面的坐标,length = data.size()只是判断时调用
{
if (length <= 0 || start < 0 || end >= length)//判断异常情况
{
throw new std::exception("Invalid Parameters");
}
int index = (start + end) / 2;//这个可以是任意值,只要在start和end之间
swap(data[index], data[end]);===在数组中选择的那个数字暂存在最后一位
int small = start - 1;//small是选出的那个数字,排序后应该所在的位置下标
for (int i = start; i < end; ++i)//进行遍历:small的递增找到排序后应该所在的位置下标,Swap将比选出的值小的数放在前面
{
if (data[i] < data[end])
{
++small;
if (small != i)
{
swap(data[i], data[small]);//把小值放在“排序后应该所在的位置下标”的前面
}
}
}
small++;
swap(data[small], data[end]);===暂存在最后一位的那个数字换到应在的位置
return small;
}
//
/// 链表归并排序 /
/
#if 链表排序
class ListNode
{
public:
int val;
ListNode* next;
ListNode(int x): val(x), next(nullptr){}
};
void create(ListNode* guard)
{
ListNode* p = guard;
p->next = new ListNode(1); p = p->next;
p->next = new ListNode(5); p = p->next;
p->next = new ListNode(9); p = p->next;
p->next = new ListNode(8); p = p->next;
p->next = new ListNode(5); p = p->next;
p->next = new ListNode(2); p = p->next;
p->next = new ListNode(3); p = p->next;
p->next = new ListNode(6); p = p->next;
p->next = new ListNode(4); p = p->next;
p->next = new ListNode(7); p = p->next;
}
void merge(ListNode* guard, ListNode* middle)
{
ListNode* p = guard;
ListNode* l1 = guard->next;
ListNode* l2 = middle->next;
middle->next = nullptr;
while (l1 != nullptr && l2 != nullptr)
{
if (l1->val < l2->val)
{
p->next = l1;
l1 = l1->next;
}
else
{
p->next = l2;
l2 = l2->next;
}
p = p->next;
}
p->next = nullptr;
if (l1 != middle->next)
p->next = l1;
if (l2 != nullptr)
p->next = l2;
}
void mergeSort(ListNode* guard, ListNode* last)
{
if (last == guard->next)
return;
ListNode* walker = guard;
ListNode* runner = guard;
while (runner != last)
{
walker = walker->next;
runner = runner->next;
if(runner != last)
runner = runner->next;
}
ListNode* middle = walker;
mergeSort(guard, middle);
mergeSort(middle, last);
merge(guard, middle);
}
int main()
{
ListNode* guard = new ListNode(0);
create(guard);
ListNode* p = guard;
while (p->next)
{
p = p->next;
}
ListNode* last = p;
mergeSort(guard, last);
return 0;
}
#endif