4

[排序算法] 快速排序 (C++) (含三种写法) - Amαdeus

 1 year ago
source link: https://www.cnblogs.com/MAKISE004/p/16909610.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

快速排序解释

快速排序 Quick Sort 与归并排序一样,也是典型的分治法的应用。 (如果有对 归并排序还不了解的童鞋,可以看看这里哟~ 归并排序)❤❤❤

1|0快速排序的分治模式

1、选取基准值,获取划分位置。将原数组 a[l, r] 划分为两个子数组 a[l, mid - 1]a[mid + 1, r]。在前一个数组中所有元素都小于等于 a[mid],后一个数组中所有元素都大于等于 a[mid]。而此时的 a[mid] 的值就是我们所取的基准值,mid 就每次划分的位置;

2、递归调用快速排序函数,分别对两个子数组 a[l, mid - 1]a[mid + 1, r] 排序;

3、快速排序我们是在原数组上进行操作的,所以我们并不需要合并,最后 a[l, r] 已经有序。

1|0快速排序的三种写法

快速排序比较普及的有三种写法,分别是 左右指针法 挖坑法前后指针法。主要是取得划分位置实现的方法有所不同
接下来会逐个介绍这三种快速排序的写法。


左右指针法

左右指针法步骤

1、首先 我们一般选取最左边的元素作为基准值 key

2、然后我们需要定义两个变量 ij
其中 i 为左指针(其实不是指针啦,只是为了方便这么叫它😋),初始 i = 0。 j 为右指针,初始 j = r - 1,向左遍历不断找到小于基准值 key 的元素。

3、我们先动右指针 j 向左遍历直到找到小于当前基准值 key 的元素;然后我们再动左指针 i 向右遍历直到找到大于当前基准值 key 的元素。
ij 分别找到它们要找的元素时,我们需要将两个元素进行位置交换。( 在这个过程中我们要保持 i < j )

4、重复步骤3,直到最后我们可爱的左右指针相遇,这时我们再将基准值 key,放到这两个指针指向的位置。此时我们就得到了当前划分的位置,基准值 key 也完成了归位。

1|0左右指针法移动指针先后顺序问题

在上述步骤中,有些人会感到疑惑,那就是我们再移动指针时,每次都要 先移动右指针再移动左指针。为什么呢?😕😕😕

在取基准值时,我们一般都是将序列的最左边位置的元素作为基准值。我们每次交换完元素后,左右指针都会继续寻找他们要找的值,观感上就是相互靠近。而问题就出在我们退出循环的那一刻

我们一直保持 i < j,也就是说,我们会在 i == j 时退出循环。假设 在某次交换之后,此时 j 指向的是交换之后的一个大于基准值的元素,如果我们先动左指针 i 去寻找一个大于基准值的元素,然鹅还未找到就已经和右指针 j 相遇了,这个时候我们需要退出循环,交换基准值 key 到当前两个指针指向的位置。
但是!!!,此时 ij 指向的是大于基准值的元素,那么我们进行交换基准值位置操作后,这个大于基准值的元素就被换到了序列的最左端,很明显,这时候出现了非常非常非常严重的错误。

那如果我们先动右指针 j,去寻找一个小于基准值的元素,然鹅没有找到就已经和左指针 i 相遇了,这个时候退出循环,ij 指向的一定是一个小于等于基准值的值。

究其原因,这其实是我们取最左边的元素作为基准值导致的。我们需要保证每次交换过来的元素是小于等于基准值的,所以我们先动右指针再动左指针


左右指针法动态演示

1|0我们以序列 [3,14,6,1,2,17,7] 为例进行动态演示。(后面还会有先动左指针的错误演示)

1|0左右指针法正确演示

3039354-20221120200252546-567967815.png

1|0左右指针法错误演示

3039354-20221120200309567-62512051.png

左右指针法核心代码



//左右指针法 int Partition_Hoare(vector<int> &a, int left, int right){ int i = left; int j = right; int key = a[left];

while(i != j){ while(i < j && a[j] >= key) //向左找到小于基准值的值的下标 j--; while(i < j && a[i] <= key) //向右找到大于基准值的值的下标 i++; swap(a[i], a[j]); } /* i等于j时跳出循环 当前基准值此时在下标为i的位置(合适的位置) */ swap(a[left], a[i]); //最左边的元素变为处于当前合适位置的元素,把基准值放在合适位置 return i; //返回合适位置(i,j都可以) }


挖坑法

挖坑法步骤

挖坑法顾名思义,就是每次挖一个坑填入元素使其归位。和左右指针法一样,我们同样需要两个变量 ij
1、我们一般取最左边的元素为基准值,然后在基准值处挖空(挖坑)

2、先动右指针 j 向左找到一个小于基准值的元素,然后将其填入先前挖的坑中,同时 j 指向的位置也多出来一个坑位。

3、再动左指针 i 向右找到一个大于基准值的元素,然后将其填入先前挖的坑中,同时 i 指向的位置又多出来一个坑位。

4、重复上述操作,直到最后两个指针相遇,此时再在这个坑位上填入基准值,即完成了基准值的归位,也就是我们划分的位置。

(具体实现这个挖坑填坑的方式可以是元素值的覆盖,也可以是元素交换的形式,在后面的核心代码中会给出这两种方式)

挖坑法动态演示

1|0我们以序列 [3,14,6,1,2,17,7] 为例进行动态演示。

1|0图(1)

3039354-20221120201745515-1870665393.png

1|0图(2)

3039354-20221120201803028-1927612119.png

挖坑法核心代码



//挖坑法1 int Partition_DigI(vector<int> &a, int left, int right){ int i = left; int j = right; int key = a[left];

while(i != j){ while(i < j && a[j] >= key) //必须先动右边的 因为取的基准值在左边 j--; //向左寻找直到找到比基准值小的 a[i] = a[j]; //挖坑 填入比基准值小的值

while(i < j && a[i] <= key) //再动左边的 i++; //向右寻找直到找到比基准值大的 a[j] = a[i]; //挖坑 填入比基准值大的值 } /* i等于j时跳出循环 下标为i的位置即为合适的插入位置 */ a[i] = key; //在i,j相遇的地方填入基准值 return i; }

//挖坑法2 int Partition_DigII(vector<int> &a, int left, int right){ int i = left; //从左边开始 int j = right; //从右边开始 int key = a[left]; //将最左边的数设为基准值

while(i != j){ while(i < j && a[j] >= key) //必须先动右边的 因为取的基准值在左边 j--; //向左寻找直到找到比基准值小的 swap(a[i], a[j]); //交换 i++; //看下一个(可以省略)

while(i < j && a[i] <= key) //再动左边的 i++; //向右寻找直到找到比基准值大的 swap(a[i], a[j]); //交换 j--; //看下一个(可以省略) } /* 一轮循环后基准值所在位置左边的数都比基准值小,右边的都比基准值大 */ /* i等于j时跳出循环 当前基准值此时在下标为i的位置(合适的位置) */ return i; //返回基准值合适位置 }


前后指针法

前后指针法步骤

前后指针法同样需要两个指针,一个是 pre,一个是 currpre 最后找到的是划分后左半部分最后一个小于基准值的元素。
1、一开始我们一般将前指针 pre 指向待排序序列的第一个元素,后指针 curr 指向 pre 后面的一个元素。

2、然后我们让 后指针 curr 去寻找小于当前基准值的元素,若找到这个元素,此时需要将这个小于基准值的元素和当前 前指针 pre 后一个的位置的元素进行交换,即与 pre + 1 位置的元素进行交换,同时前指针 pre 也往后移动一步。

3、在上述过程中我们需要保持 curr <= r,当 curr > r 退出循环时,需要将当前 pre 所指向的元素和基准值进行交换。最后返回这个pre,即为我们划分的位置。


前后指针法动态演示

1|0我们以序列 [3,14,6,1,2,17,7] 为例进行动态演示。

1|0图(1)

3039354-20221120203738510-1690193373.png

1|0图(2)

3039354-20221120203745963-695196626.png

前后指针法核心代码



//前后指针法 int Partition_PreAndCurr(vector<int> &a, int left, int right){ int pre = left; //pre最后找到左半部分最后一个小于基准值的元素 int curr = pre + 1; //curr找到pre指向元素之后的下一个小于基准值的元素 int key = a[left];

while(curr <= right){ if(a[curr] < key && ++pre != curr) //当a[curr]>=key时pre保持不动,直到最后curr找到下一个小于基准值的元素 swap(a[curr], a[pre]); //pre往后移动到其下一个位置,并交换前后指针指向的元素 curr++; } swap(a[left], a[pre]); //最后将基准值和此时处于pre位置的小于基准值的元素交换 return pre; }


快速排序时间复杂度

假设时间复杂度为 T(n)。在进行取得划分位置上我们需要消耗 O(n) 的时间,在递归调用快速排序函数上,最好情况下如果每次我们取得的划分位置为n/2的位置,即消耗了 2T(n/2) 的时间。最坏情况下如果每次我们取得的划分位置为首或尾的位置,即消耗 T(n-1)+T(0)的时间。
故时间复杂度如下

1|0最优时间复杂度

3039354-20221120205648639-367871772.png

1|0最坏时间复杂度

3039354-20221120205705145-688818162.png

取基准值优化

如何高效取基准值

对于取基准值,我们发现 如果当前所取的基准值恰好是当前序列中最小的数或者最大的数,那么我们的划分位置必然是首或尾的位置,如果每次都这个亚籽,我们消耗的时间会非常的多,这显然就是最坏情况下的时间复杂度的状况。

1|0那么我们如何尽量地避免这件事呢?🤔🤔🤔

我们自然会想到随机取数,这固然是一个很好的办法,但是这个世界充满了无限可能性。假如我们原先最左边的元素并不是最小或最大的一个元素,但是随机取到的元素却恰好是那个最小或最大的元素呢?

(当然随机取数这个方法也是值得尝试的,后续程序中我不会用这个方式实现取基准值的优化,如果有需要,只需要把取基准值的几行代码改成如下就可以了)



int random = rand() % (right - left + 1) + left; swap(a[random], a[left]); int key = a[left]

1|0为了避免这种充满戏剧性的情况,我们采取 三数取中 的方法取基准值。

三数取中,就是取序列中的最左边,中间,以及最右边三个数进行比较,以这三个数大小为中的元素作为基准值。这样可以更大程度上避免划分位置不好的情况,即使其并不能彻底避免,但是是一个很好的办法。


取基准值优化核心代码



int GetMid(int a[], int left, int right){ int mid = (left + right)>>1; if(a[left] < a[mid]) if(a[mid] < a[right]) return mid; //mid为中间值 else if(a[left] < a[right]) return right; //right为中间值 else return left; //left为中间值 else if(a[mid] > a[right]) return mid; //mid为中间值 else if(a[left] > a[right]) return right; //right为中间值 else return left; //left为中间值 }


完整程序源代码



#include<iostream> #include<vector> #include<time.h> using namespace std;

/* 1 */ //左右指针法 int Partition_Hoare(vector<int> &a, int left, int right){ int i = left; int j = right; int key = a[left];

while(i != j){ while(i < j && a[j] >= key) //向左找到小于基准值的值的下标 j--; while(i < j && a[i] <= key) //向右找到大于基准值的值的下标 i++; swap(a[i], a[j]); } /* i等于j时跳出循环 当前基准值此时在下标为i的位置(合适的位置) */ swap(a[left], a[i]); //最左边的元素变为处于当前合适位置的元素,把基准值放在合适位置 return i; //返回合适位置(i,j都可以) }

/* 2 */ //挖坑法1 int Partition_DigI(vector<int> &a, int left, int right){ int i = left; int j = right; int key = a[left];

while(i != j){ while(i < j && a[j] >= key) //必须先动右边的 因为取的基准值在左边 j--; //向左寻找直到找到比基准值小的 a[i] = a[j]; //挖坑 填入比基准值小的值

while(i < j && a[i] <= key) //再动左边的 i++; //向右寻找直到找到比基准值大的 a[j] = a[i]; //挖坑 填入比基准值大的值 } /* i等于j时跳出循环 下标为i的位置即为合适的插入位置 */ a[i] = key; //在i,j相遇的地方填入基准值 return i; }

//挖坑法2 int Partition_DigII(vector<int> &a, int left, int right){ int i = left; //从左边开始 int j = right; //从右边开始 int key = a[left]; //将最左边的数设为基准值

while(i != j){ while(i < j && a[j] >= key) //必须先动右边的 因为取的基准值在左边 j--; //向左寻找直到找到比基准值小的 swap(a[i], a[j]); //交换 i++; //看下一个(可以省略)

while(i < j && a[i] <= key) //再动左边的 i++; //向右寻找直到找到比基准值大的 swap(a[i], a[j]); //交换 j--; //看下一个(可以省略) } /* 一轮循环后基准值所在位置左边的数都比基准值小,右边的都比基准值大 */ /* i等于j时跳出循环 当前基准值此时在下标为i的位置(合适的位置) */ return i; //返回基准值合适位置 }

/* 3 */ //前后指针法 int Partition_PreAndCurr(vector<int> &a, int left, int right){ int pre = left; //pre最后找到左半部分最后一个小于基准值的元素 int curr = pre + 1; //curr找到pre指向元素之后的下一个小于基准值的元素 int key = a[left];

while(curr <= right){ if(a[curr] < key && ++pre != curr) //当a[curr]>=key时pre保持不动 swap(a[curr], a[pre]); //pre往后移动到其下一个位置,并交换前后指针指向的元素 curr++; } swap(a[left], a[pre]); //最后将基准值和此时处于pre位置的小于基准值的元素交换 return pre; }

/* 4 */ //取基准值的优化 三数取中 int GetMid(int a[], int left, int right){ int mid = (left + right)>>1; if(a[left] < a[mid]) if(a[mid] < a[right]) return mid; //mid为中间值 else if(a[left] < a[right]) return right; //right为中间值 else return left; //left为中间值 else if(a[mid] > a[right]) return mid; //mid为中间值 else if(a[left] > a[right]) return right; //right为中间值 else return left; //left为中间值 }

//左右指针法 三数取中优化 int Partition_Better(vector<int> &a, int left, int right){ int pos = GetMid(a, left, right); swap(a[pos], a[left]); //将取得的基准值和第一个位置交换

int i = left; int j = right; int key = a[left]; while(i != j){ while(i < j && a[j] >= key) j--; while(i < j && a[i] <= key) i++; swap(a[i], a[j]); }

swap(a[i], a[left]); return i; }

/*******************************************************************/ void Quick_Sort(vector<int> &a, int left, int right){ if( left >= right ) return;

//int i = Partition_Hoare(a, left ,right); //分割 C(n) = n - 1 //int i = Partition_DigI(a, left, right); //int i = Partition_DigII(a, left, right); //int i = Partition_PreAndCurr(a, left, right);

int i = Partition_Better(a, left, right); Quick_Sort(a, left, i - 1); //递归左边的部分 Quick_Sort(a, i + 1, right); //递归右边的部分 }

void show(vector<int> &v){ for(auto &x : v) cout<<x<<" "; cout<<endl; }

main(){ vector<int> v; srand((int)time(0)); int n = 50; while(n--) v.push_back(rand() % 100 + 1); show(v);

Quick_Sort(v, 0, v.size() - 1);

cout<<endl<<endl; show(v); }


1|0程序运行结果图

3039354-20221120212157370-1124856742.png

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK