很水而且还很鸽的学习笔记,不定期更新
无限猴子定理 :
一只猴子随机敲打打字机键盘,如果时间足够长,总是能打出特定的文本,比如莎士比亚全集
最近更新:2018年12月6号,更新三向快速排序原理代码
下列均以排成升序序列为目标的代码,若想做降序请自行修改XD
目录
选择排序:
简单粗暴,最为超时
- 选数组里最小的数,把它放在第零位
- 从第一位开始选数组里最小的数,把他放在第一位
- …直到最后一位
void SelectionSort(){
for(int i=0; i<n; i++){
int jShu = i;
for(int j=i+1; j<n; j++) if(A[j]<A[jShu]) jShu = j;
swap(A[i], A[jShu]);
}
}
swap(a, b) 函数用来交换参数里面的两个数 a 和 b ,后面经常用
冒泡排序:
虽然很慢,但是很有用
- 从数组第零位开始向后枚举判断,如果这位比下一位要大,则交换
( 这样其实就是变相的找最大值,如果你尝试模拟一下10个数的排序,你会发现一边下来以后,最大的数浮到了最后面 ) - 接着数组最后一位变成了最大的数,接下来在从第零位开始向后枚举直到最后一位的前面,然后第二大的数就到了倒数第二位
( 最后一位就不用去判断了,反正都最大了,这样后面会节省点时间 ) - 接着枚举,直到都排好序
void BubbleSort(){
for(int i=n-1; i>=0; i--)
for(int j=0; j<i; j++)
if(A[j]>A[j+1]) swap(A[j],A[j+1]);
}
插入排序:
虽然也很慢,但是更有用,很多时候在小数据排序的情况下,冒泡排序是很方便很稳妥的排序方案
- 从第一位开始向前检索,把这一位插入到前面是比这位小,后面是比这位大的位置
( 之所以不从第零位开始就是因为前面没有任何数,那岂不是我插我自己? ) - 从第二位开始向前检索,依旧按照上面的方法插入,你会发现这么插入后形成的都是升序列
( 似乎是废话XD ) - 直到最后一位,向前再检索一边,Over,从头到尾全是升序列了
那么怎么找到前面是比这位小,后面是比这位大的位置呢?
实际上我们可以看出来我每次插入后,这个数组都是升序的,也就是说我只要不断向前比较,如果比较到前面一个数比我要插入的数还小,就直接插入到这个数后面就可以了
两种代码实现:
//原版
void InsertionSort1(){
int bet, jShu;
for(int i=1; i<n; i++){
bet = A[i];
jShu = i;
for(int j=i-1; j>=0&&bet<A[j]; j--){
jShu = j;
A[j+1] = A[j];
}
A[jShu] = bet;
}
}
//简化版
void InsertionSort2(){
for(int i=1; i<n; i++) for(int j=i-1; j>=0&&A[j+1]<A[j]; j--) swap(A[j+1],A[j]);
}
鸡尾酒排序
冒泡排序的升级版本 —— 左右摇摆!
- 从数组第零位开始向后枚举判断,如果这位比下一位要大,则交换
- 接着不回到第零位,而是从倒数第二位开始向前冒泡,把小的数浮到前面去
( 就是左右摇摆版的冒泡排序 )
这种方式比普通冒泡快了那么一点点,虽然耗时基本没有区别,但是鸡尾酒排序可以做出很骚的操作,比如说改一改代码就能排出一个以中间为 最大值(最小值),左右 渐小(大) 的数列
void CocktailSort(){
int left=0, right=n-1;
while(left<right){
for(int i=left; i<right; i++) if(A[i]>A[i+1]) swap(A[i],A[i+1]);
right--;
for(int i=right; i>left; i--) if(A[i]<A[i-1]) swap(A[i],A[i-1]);
//若改为if(A[i]>A[i-1])可以做成以中心为最小值向两边扩散的数列排序!
left++;
}
}
如下以中心为最小值向两边扩散的数列
希尔排序
非常常用的排序,插入排序的升级版本 —— 跳位检索,最为致命!
利用希尔增量序列的希尔排序时间复杂度最坏是 O ( n 2 ) O (n^2) O(n2),平均时间复杂度是 O ( n 1.3 ) O (n^{1.3}) O(n1.3),属于不稳定排序
前言:
首先我们看一下插入排序,之所以它的效率缓慢,就是因为某个数在插入的过程中,必须一位位检索,如果目标点很远,往往需要遍历的很远才能找到指定位置,那么我们是不是可以跳位来检索呢?
说到快速检索指定位置,可能大部分人第一想到的就是二分查找,的确,我们可以通过二分查找来快速找到目标点 ( 因为对于当前你要插入的这个数来说,它前面的数据是有序的,可以利用二分查找 )
但是当我们辛辛苦苦用
O
(
log
2
n
)
O (\log_2 n)
O(log2n)找到了指定位置
结果问题来了,我该怎么插入这个数据呢?
与一位位检索不同,二分查找只能找到位置,而一位位检索则在检索过程中把数据一位位往后移,使得找到后可以直接插入,所以说二分查找实际上并没有简化整体时间复杂度,因为你最后还是要一位位转移数据,空出空来插入,它只是在一定程度上简化了查找的时间
那么这个时候希尔排序就出现了!
该排序实质上是一种分组插入方法
希尔排序:
竟然我二分查找不行,那就另换思路!
怎么样让数据不一步步蹭到指定位置,又可以准确的找到指定位置呢,把数组分组怎么样?
假如说一个 10 数据的乱序数组,为了使数据大跨步,我先把数组分为 5组 ,每组 2个元素
如下图 ( 实在太懒摘用一下百度图XD )
592和72一组
401和911一组
…
然后给每组数据为一个单位,做插入排序,如,给 592和72 做一次插入排序 ( 下标增量是5 ) 变成了 72和592这样我们实际上就是用1步做到了移动5步
PS:直接用增量5的下标差换一下位置,并不用一个个蹭过去
给剩下几组做同样的事情,我们会发现小的数据仅用了几步,就基本上移到了前面
可是这样还不够呀!
我需要更精确的排序
接下来分为 2组 ,每组 5个元素
再给每组来一遍插入排序 (中跨步微调)
这个时候每次增量为2,效率还是稍微高一点的
最后
整体数据已经比较有序了,用正常的插入过一遍整个数组就行,这时候很省时,因为整体很有序
(可能这时候有人不太理解了,你最后也是一边完整的插入排序,这时间复杂度不是更高了么,实际上你可以这么想,我只不过是把插入排序拆分开了,在前面无论是间隔4,还是间隔1,间隔0的排序,合起来就是原来插入排序的整体过程,但是在我间隔4和间隔2的时候,我跨越了很多数据,使得整体遍历的时间变短了,而最后一边间隔0的排序,实际上只动了那么几个元素,总体上,时间复杂度小很多 )
增量序列:
值得注意的是,我上面的选择了5组,2组,1组这种方式,实际上就是元素个数10不断除2的结果
这时候我们称这个叫 希尔增量序列,是 {
N
/
2
,
N
/
4
,
.
.
.
,
1
{N/2 ,N/4 , ... ,1}
N/2,N/4,...,1 }
但实际上这种序列并不是很有效,很多时候我分组时跨的步数过大,使得后续排序效率变低
那么有没有更高效的方式来遍历,有!
Hibbard增量序列:{
1
,
3
,
.
.
.
,
2
k
−
1
1, 3, ..., 2^k-1
1,3,...,2k−1}
Sedgewick增量序列:{
1
,
5
,
19
,
41
,
109
,
.
.
.
1, 5, 19, 41, 109,...
1,5,19,41,109,...}
其中数据由
9
×
4
i
−
9
×
2
i
+
1
9 \times 4^i - 9 \times 2^i + 1
9×4i−9×2i+1 和
2
i
+
2
×
(
2
i
+
2
−
3
)
+
1
2^{i+2}\times(2^{i+2}-3)+1
2i+2×(2i+2−3)+1 两个算式生成,i 取正整数
这些经过实践,要比纯粹的希尔增量序列快一些,特别是Sedgewick增量序列
代码:
- 首先做一个循环,用来枚举不用的增量det,通过增量来分组,如5,2,1这种分组
- 然后再嵌套一个循环来枚举每个组
- 对每个组做一次插入排序,这里跟插入排序唯一的差别就是插入是一个个向前检索,但是这个插入是以增量det为间隔向前检索
使用希尔增量序列的代码
void ShellSort_Shell(){
for(int det=n/2; det>=1; det/=2)
for(int i=0; i<det; i++)
for(int j=i+det; j<n; j+=det)
for(int x=j; x-det>=0&&A[x]<A[x-det]; x-=det) swap(A[x-det],A[x]);
}
使用Sedgewick增量序列的代码
void ShellSort_Hibbard(){
int jShu = 1;
jZhi[0] = 1;
for(int i=1; jZhi[jShu-1]<n; i++){
jZhi[jShu++] = pow(2,i+2)*(pow(2,i+2)-3)+1;
if(jZhi[jShu-1]>=n) break;
jZhi[jShu++] = 9*pow(4,i)-9*pow(2,i)+1;
}
//上面那个就是构造Sedgewick增量序列的过程,你也可以手工打表
//pow(a,b)函数,用来算a^b
for(int i=0; i<jShu; i++){
int det=jZhi[jShu-1-i];
for(int j=0; j<det; j++)
for(int x=j+det; x<n; x+=det)
for(int y=x; y-det>=0&&A[y]<A[y-det]; y-=det) swap(A[y-det],A[y]);
}
}
可以看到优化后还是快一点的
快速排序:
冒泡排序的改进版本,排序算法中的dalao,据说是内部适用性最好的排序
平均时间复杂度 O ( n ⋅ l o g 2 n ) O (n\cdot{log_2n}) O(n⋅log2n),最坏时间复杂度 O ( n 2 ) O (n^2) O(n2)
大体思路:
快速排序利用二分的思想,先找一个基准值,通过基准值把数组不断的二分,再在之后的小数组上重复步骤
比如说
4 | 9 | 5 | 0 | 7 | 2 | 1 | 3 | 8 | 6 |
---|---|---|---|---|---|---|---|---|---|
↑ |
假如说我总以最后一个元素为基准值 ( 常被称作Key,在代码中我写作Bet ) ,这个数组是6
这个时候我把比6小的放在6的前面,把比6大的放在后面
最后可以得到
4 | 5 | 0 | 2 | 1 | 3 | 6 | 9 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
↑ |
可以看到这个时候我的6本身是到达指定位置了的,那么这时候就以6为边界分开左右数组
4 | 5 | 0 | 2 | 1 | 3 | 边界 | 9 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
↑ | 边界 | ↑ |
两个小数组,在进行同样的操作 以最后一个元素为基准值,这两个数组分别是3和8 处理过后有
0 | 2 | 1 | 3 | 4 | 5 | 边界 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
↑ | 边界 | ↑ |
再以3和8为边界左右分开这两个数组。。。
直到分得的数组长度<=1,就不再分
最后整体就是有序的了
听起来很简单,但是那该怎么实现呢?
实现方法:
左右指针式写法快排:
-
首先我们需要不断地以区间为分界分开数组,这时候就要用到递归,怎么在同一个数组上处理成不同的小数组?显然我们需要知道每个小数组的首元素下标和尾元素下标 (在下段代码中是begin和end),以这个为依据来分开数组
-
之后我们需要取一个基准值来分开数组,这个值我们通常取每个数组的首元素,尾元素或者中间元素
( 下面代码我们先以取中间元素为例 ) -
然后设置两个左右指针Right和Left,一个向左遍历,一个向右遍历,直到它们相遇 每当Left指针找到一个比基准值大的,Right指针找到一个比基准值小的,则交换这两个数,一边下来以后,整个数组就以基准值为边界分开了左右两边,而且Left和Right指针也一定会在基准值的位置相遇
取中值为基准值版
void QuickSortA(int begin, int end){
int Left = begin, Right = end;
int Bet = A[(begin+end)/2];
while(Left <= Right){
while(A[Left] < Bet) Left++;
while(A[Right] > Bet) Right--;
if(Left <= Right) swap(A[Left++], A[Right--]);
}
if(begin < Right) QuickSortA(begin, Right);
if(Left < end) QuickSortA(Left, end);
}
三值优化版
但是实际上无论是以中值为基准值还是以首元素,尾元素为基准值都有一定弊端
具体表现就是当我以首元素为基准值,如果这个序列本身就是一个升序列的类似序列,此时的快排就会退化成冒泡排序,变成奇慢无比的
O
(
n
2
)
O (n^2)
O(n2)
( 每次二分只能分出一个数组,因为首元素就是最小值,所以只有一边 )
同样的以尾元素和中间元素也会面临同样的问题,他们也会遇到一种相似的序列,使得其退化成
O
(
n
2
)
O (n^2)
O(n2)
( 其实你怎么取基准值总会有一种序列使你的快排退化成
O
(
n
2
)
O (n^2)
O(n2),只不过首元素,尾元素,中间元素这种退化序列容易被构造出来)
所以说为了满足绝大部分条件,最好的方法是每次都取一个随机位置的基准值,而不是固定基准值,这样的话遇到一个使其退化的序列简直是万年一遇,特别是大量数据的情况
然而生成随机数需要额外的时间复杂度,有些得不偿失
所以三值优化法是最常用的快排取基准值方法
原理很简单,就是在首元素,尾元素,中间元素中取中间大小的那个值作为基准值,简单粗暴,满足绝大部分要求
#define MAX(x,y) ((A[x] > A[y])?x : y)
#define MIN(x,y) ((A[x] < A[y])?x : y)
#define MID(x,y,z) x+y+z-MAX(x,MAX(y,z))-MIN(x,MIN(y,z))
void QuickSortA(int begin, int end){
int Left = begin, Right = end;
int Bet = A[MID(begin, end, (begin+end)/2)];
while(Left <= Right){
while(A[Left] < Bet) Left++;
while(A[Right] > Bet) Right--;
if(Left <= Right) swap(A[Left++], A[Right--]);
}
if(begin < Right) QuickSortA(begin, Right);
if(Left < end) QuickSortA(Left, end);
}
前后指针式写法快排:
前后指针法比较特殊,跟挖坑法不同,它更适合于遍历链表
整体流程:
稍微有些难以理解XD,建议手动尝试一边
首先我们要声明两个指针( 这里我们用数组下标代替 ),一个是 Pre ,一个是 Cur
首先,对于每个数组,我们先以末尾元素为基准值,把 Cur 初始值设置为第一个元素的数组下标,Pre 的初始值则是 Cur-1
然后通过 Cur 指针来遍历数组
- 如果 Cur 指向的元素比基准值要大,则继续遍历;
- 如果 Cur 指向的元素比基准值要小,则先将 Pre 的值+1,再判断一下Cur 指向的元素和 Pre 指向的元素是否相等,如果不相等就交换,如果相等就继续遍历
最后交换 Pre 指针+1指向的元素和左右一个末尾元素,Pre 指针的指向的位置就是分开数组的中间值
关于前后指针法的解释:
为何这种方式可以做到把比基准值小的和比基准值大的分开在基准值两边?
我们可以这么思考,假如说现在我把重点放在如何找到一个位置,使得其能分开左右两边(一次快排之后,小的都在左面,大的都在右面)
那么Pre指针就是做这个事情
比如说
4 | 5 | 9 | 0 | 7 | 2 | 1 | 3 | 8 | 6 |
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Cur指针先遍历第一个元素4,我们知道这个是元素比基准值6小,所以说以目前的情况来看,Pre指向的位置至少是0位置的后面
4 | pre | 5 | 9 | 0 | 7 | 2 | 1 | 3 | 8 | 6 |
---|---|---|---|---|---|---|---|---|---|---|
0 | pre | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Cur指针遍历第二个元素5,依然比基准值小,这个时候我知道Pre指向的位置至少是1位置的后面
4 | 5 | pre | 9 | 0 | 7 | 2 | 1 | 3 | 8 | 6 |
---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | pre | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
PS:这两步为了好理解没有加入交换步骤,不过可以看到如果都是比基准值小的,即使加入交换步骤也不会有什么问题
Cur指针遍历第三个元素9,比基准值大了,那我们要保证它在基准值的右面,那么这个时候Pre指针不动,但我们现在知道它的位置需要隔开5和9
4 | 5 | 0 | pre | 9 | 7 | 2 | 1 | 3 | 8 | 6 |
---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | pre | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Cur指针遍历第四个元素0,又比基准值小,那我们要保证它在基准值左面,但是我们知道前面有个9,这时候交换0和9,再让指针指向2的后面隔开 4 5 0 和 9
4 | 5 | 0 | pre | 9 | 7 | 2 | 1 | 3 | 8 | 6 |
---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | pre | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Cur指针遍历第五个元素7,比基准值大,要保证它在基准右面,所以pre指针不动
Cur指针遍历第六个元素2,比基准值小,保证其在基准值左面,所以交换 2 和 9 ,再把pre指针指向 2 后面
4 | 5 | 0 | 2 | pre | 7 | 9 | 1 | 3 | 8 | 6 |
---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | pre | 4 | 5 | 6 | 7 | 8 | 9 |
…
下面几个元素同理,可以见到最后一步只需要把末尾元素(基准值) 放到pre指向的位置即可,这样就构造出了合适的形式,所以说我们可以总结出两条
- 如果 Cur 指向的元素比基准值要大,则继续遍历;
- 如果 Cur 指向的元素比基准值要小,则先将 Pre 的值+1,再判断一下Cur 指向的元素和 Pre 指向的元素是否相等,如果不相等就交换,如果相等就继续遍历
常用版本,以末尾为基准值
void QuickSortB(int begin, int end){
int Pre = begin-1, Cur = begin;
int Bet = A[end];
while(Cur < end){
if(A[Cur]<Bet && A[Cur]!=A[++Pre]) swap(A[Pre], A[Cur]);
Cur++;
}
swap(A[++Pre], A[end]);
if(begin < Pre-1) QuickSortB(begin, Pre-1);
if(end > Pre+1) QuickSortB(Pre+1, end);
}
三值优化版
#define MAX(x,y) ((A[x] > A[y])?x : y)
#define MIN(x,y) ((A[x] < A[y])?x : y)
#define MID(x,y,z) x+y+z-MAX(x,MAX(y,z))-MIN(x,MIN(y,z))
void QuickSortB(int begin, int end){
int Pre = begin-1, Cur = begin;
int Shu = MID(begin, end, (begin+end)/2);
int Bet = A[Shu];
swap(A[Shu], A[end]); //把合适的基准值和尾元素交换,然后其他代码照搬XD
while(Cur < end){
if(A[Cur]<Bet && A[Cur]!=A[++Pre]) swap(A[Pre], A[Cur]);
Cur++;
}
swap(A[++Pre], A[end]);
if(begin < Pre-1) QuickSortB(begin, Pre-1);
if(end > Pre+1) QuickSortB(Pre+1, end);
}
但这还远远不够!—— 三向快排优化
现在我们解决了选取基准值的问题,让大部分数据不会退化成
O
(
n
2
)
O (n^2)
O(n2) ,也给出了适合链表的快排方式,但我们是不是忘记了些什么?
没错,如果一组数据有大量重复数据怎么办,或者说更极端一些,一个数组中几乎全是重复数据,此时用
O
(
n
⋅
l
o
g
2
n
)
O (n\cdot{log_2n})
O(n⋅log2n) 来排序岂不是有点亏!
这时候就要让我们的 三向快排 出场了 (其实这部分内容性价比不高,如果不是特别需要可以略过)
void QuickSortC(int begin, int end){
if(begin>=end) return;
int Mid_l = begin, Mid_r = end, i = Mid_l+1;
swap(A[MID(begin,end,(begin+end)/2)], A[begin]);
while(i <= Mid_r){
if(A[Mid_l]>A[i]) swap(A[Mid_l++], A[i++]);
else if(A[Mid_l]<A[i]) swap(A[Mid_r--], A[i]);
else i++;
}
QuickSortC(begin, Mid_l-1);
QuickSortC(Mid_r+1, end);
}
归并排序
- 待更
void Merge(int Left, int Bet, int Right){
int *temp = new int[Right-Left+1];
int jShu = 0, jShuA = Left, jShuB = Bet+1;
while(jShuA <= Bet&&jShuB <= Right) temp[jShu++] = A[jShuA]<=A[jShuB] ? A[jShuA++] : A[jShuB++];
if(jShuA <= Bet) for(int i=jShuA; i<=Bet; i++) temp[jShu++] = A[i];
if(jShuB <= Right) for(int i=jShuB; i<=Right; i++) temp[jShu++] = A[i];
for(int i=0; i<jShu; i++) A[Left++] = temp[i];
}
//递归版
void MergeSort1(int begin, int end){
if(begin<end){
int bet = (begin+end)/2;
MergeSort1(begin, bet);
MergeSort1(bet+1, end);
Merge(begin, bet, end);
}
}
//循环版
void MergeSort2(int Len){
for(int i=1; i<Len; i*=2){
int Left = 0;
while(Left+i<Len){
int Bet = Left+i-1;
int Right = Bet+i>Len ? Len-1 : Bet+i;
Merge(Left, Bet, Right);
Left = Right+1;
}
}
}
堆排序
- 待更
计数排序
- 待更