排序方法总结--后序补充稳定性概念和其他集中排序方法的概念,包括堆排序
前言:
关于排序我觉得有篇博客讲的比较全面,有空可以看看分类刷题(三):排序(Sort) ,本文的话主要介绍面试场用的快速排序、冒泡排序和归并排序。
1.快速排序
快速排序其实是在冒泡排序的基础上做出的一个改进。快速排序的基本思想是:
1、先从数列中取出一个数作为基准数,一般是数列最左边的start
2、分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边
3、再对左右区间重复第二步,直到各区间只有一个数
首先我们先建立一组数据:主要有三个参数,i为区间的开始地址,j为区间的结束地址,X为当前的开始的值.
1.1搜索流程
在快速排序中我们先以一个例子来分析,就是我们的X是我们的start值,我们要将X记录下来,然后一开始从end开始,我们知道我们需要让X右侧都是比他大的元素,所以不断减少end的下标,如果比X大那符合要求就走,注意到end走的过程越多或者说后面出现的start过程走得越多,对我们的计算是好事,意味着走过的路段都是可行解了,在以后的搜索中都不用考虑了。极端情况X就是最小的值,他就在最左边,导致end–一直到start<end!!牢记
前面说了一些额外的话,好,我们继续刚才的搜索,end一直减少,直到end下标对应的数值假设是Y比X要小了,好!出现不速之客了,小的必须在我左边,所以呢我就把X和Y呼唤了,这样达到了什么功能!注意到当前X右侧都是没问题的,包含X点当前Y左侧都是没问题的,包含Y点。
接下来需要继续对start-end的区间进行搜索了(开区间)。因为之前搜素了下右侧,现在我搜索下左侧了,start不是没问题么,那start++,判断这个下标对应的数是否比end小,或者说比X小,如果小,没问题继续+,如果start++出现对应的数,假设是M,M>X,好! 出现不速之客了,必须在X右侧才对啊,那就把M和X互换,这时候X在end位置,说白了就是把end的值变成M。此时可以看到左侧收缩了,右侧还没收缩呢。
然后重复上述操作start和end之间距离不断减少,一旦出现end–到start=end或者 start++到start=end,注意到,我们说某一个区间左右都是闭合没问题的,一个边界移动另一个边界不动,重合了说明都没问题啊。而且重合的点肯定是原来的X值! 此时当start=end时,排序结束。
之前说的是互换,其实我们也可以不换,把start++ 或者end–遍历到出问题的元素覆盖到end或者start就行,去而他本来的位置应该让X来填补,我们就不填补了,因为我们的X每次都会转移转移,最终转移到start=end,我直接在这里地方给他赋值不就行了!
注意最后的start就是我们的X在原始数组中的下标。因为左边都是比他小,右边都是比他大。我们能获得原始数组中从小大到排名第start+1的元素就是nums[start]!利用这个方法,我们每次排序返回index,比如我们想找从小到大第五个元素是谁。先对nums[1]做基准,能找出他的排名,比如是4 ,4<5,那基准数在数组右侧,对右侧区间某个数做基准继续找他的排名。直到找到排名等于5即可。这个方法可以用来求Leetcode215题:数组中的第K个最大元素:
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
if(nums.empty()||k<=0||k>nums.size())
return - 1;
k = nums.size()-k; //我们是升序排序,第K个最大的反过来就是这个值,索引从0开始~
int index = 0;
int start = 0;
int end = nums.size()-1;
while(1)
{
index = quick(nums,start,end);
if(index==k) //直到索引到这个index就结束~
break;
else if(index<k)
start = index +1;
else
end = index -1;
}
return nums[index];
}
//快排分割函数
int quick(vector<int>&nums,int i,int j)
{
int start = i;
int end = j;
int temp = nums[start];
while(start<end)
{
while(nums[end]>temp && start<end)
{
--end;
}
if(start<end)
{
nums[start]=nums[end];
++start;
}
while(nums[start]<temp && start<end)
++start;
if(start<end)
{
nums[end]=nums[start];
--end;
}
}
nums[start] = temp;
return start;
}
};
除了上面的传统方法,我们基准数字可以随机选择一个,然后换到最右边,可以采用下面的程序方法:
class Solution {
public:
int quickSelect(vector<int>& a, int l, int r, int index) {
int q = randomPartition(a, l, r);
if (q == index) {
return a[q];
} else {
return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index);
}
}
inline int randomPartition(vector<int>& a, int l, int r) {
int i = rand() % (r - l + 1) + l; //rand()产生0-最大整数。这样会产生0到 r-l的整数。
swap(a[i], a[r]); //一般把基准值放在开头或者结尾,调入参数的时候方便点
return partition(a, l, r);
}
inline int partition(vector<int>& a, int l, int r) {
int x = a[r], i = l - 1;
for (int j = l; j < r; ++j) {
if (a[j] <= x) {
swap(a[++i], a[j]);
}
}
swap(a[i + 1], a[r]);
return i + 1;
}
int findKthLargest(vector<int>& nums, int k) {
srand(time(0));
return quickSelect(nums, 0, nums.size() - 1, nums.size() - k);
}
};
排序的核心程序如下,下面对这块程序做个解释:
我们将目标值放到数组最右边,x=a[r],然后j一直移动嘛,最多移动到r的左边,最右边是我们的目标值当然放到循环之后操作啦。这里i其实记录了我们已经完成从左到右的排序指标索引。一开始一个都没有所以i=l-1。然后j一直再变大,如果对应j的元素比目标值小,符合要求,那么我们成功搞完了一个数,索引i也增大一个,如果全部都是比我们最右边的数字小,那每次i都是后来加上去然后换,那i=j相当于换了个寂寞! 注意到如果遇到比我们目标值大的元素,j在变换的过程中i是不变的。知道到了r的位置,那么i+1我们知道的从工作到右第一个比目标值大的元素! 那就和我们目标值换位置不就行了。如果中间遇到符合条件得了,那i+1的值就换成那个符合条件的就行了,交换嘛。对于我们的最终目标反正肯定在i后面!
inline int partition(vector<int>& a, int l, int r) {
int x = a[r], i = l - 1;
for (int j = l; j < r; ++j) {
if (a[j] <= x) {
swap(a[++i], a[j]);
}
}
swap(a[i + 1], a[r]);
return i + 1;
}
1.2对整个数组进行快排的程序和模板!!:
之前我们的操作只叙述了对基准点进行快排后的结果。就是选择数组中一个数,快排后数组左侧都是比他小,右侧都是比他大的。那怎么实现整个数组的快速排序呢!不多说,先上模板(自己写的):
//先写一个工具人函数,他的目的是实现对不同区域的快排的组织,中介环节
void QuickSort(vector<int>& a, int low, int high)
{
if(high >= low) //或者说是 high==low
{
return;
}
int j = Partition(a, low, high); //快排的核心函数~~
QuickSort(a, low, j-1); //将小于a[j]的部分进行排序
QuickSort(a, j+1, high); //将大于a[j]的部分进行排序
}
//在工具人函数中需要调用核心函数,也就是单次快排的方式函数
int Partition(vector<int>& a, int low, int high){
int i = rand() % (r - l + 1) + l; //rand()产生0-最大整数。这样会产生0到 r-l的整数。
swap(a[i], a[r]); //一般把基准值放在开头或者结尾,调入参数的时候方便点
int x = a[r], i = l - 1;
for (int j = l; j < r; ++j) {
if (a[j] <= x)
swap(a[++i], a[j]);
}
swap(a[i + 1], a[r]);
return i + 1;
}
记住这个模板把,补充下,核心快排分割函数还可以用这种形式:
int quick(vector<int>&nums,int i,int j)
{
int start = i;
int end = j;
int temp = nums[start];
while(start<end)
{
while(nums[end]>temp && start<end)
{
--end;
}
if(start<end)
{
nums[start]=nums[end];
++start;
}
while(nums[start]<temp && start<end)
++start;
if(start<end)
{
nums[end]=nums[start];
--end;
}
}
nums[start] = temp;
return start;
}
第三种:
void quickSort(int strs[], int l, int r)
{
if(l >= r) return;
int i = l, j = r;
while(i < j) { //注意到这个方法下面事大于等于或者小于等于必须加等号,不然遇到111会先入死循环 就是表明左侧筛选大于等于的元素,右侧小于等于的元素,最终整合了就是完美的
while(strs[j]>=strs[l] && i < j) j--; //右边搜索遇到第一个刺头比他小
while(strs[i] <=strs[l] && i < j) i++; //左边搜索遇到第一个刺头比他大
swap(strs[i], strs[j]); //这两个刺头一换位置就完美了,然后走过的路都是完美的,进入下次大循环 直到i=j
}
swap(strs[i], strs[l]); //i只所及都是小于等于他的元素!没问题
quickSort(strs, l, i - 1);
quickSort(strs, i + 1, r);
}
2.归并排序
Leetcode21:合并两个有续链表求解方法中就利用了归并排序的的思想。该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。可以用下面这张图来概括分治的过程,略去递归就看最后一次排序,左右两个子部分都是已经升序排好了的,我们可以创建一个temp数组,对左右两部分建立双指针,小的进数组指针移动,每次比较ij两个指针对应元素,小的进,然后移动,大的不进不移动。一旦某个走完,数组下面的给没走完的接盘。这就是分治算法。
2.1归并排序模板!
下面是归并排序的模板程序,根据上面的思路,看起来也是很好理解的。和快速排序一样,都是存在一个空壳工具人函数,工具人函数里面包含了对单次排序的核心函数。递归过程有点像二叉树的后序遍历。如何理解呢? MergeArray函数实现对当前low high范围内以mid为分解构成的两个有序子部分进行合并排序。这属于操作。区间分别为[low,mid],[mid+1,high]。那操作之前我们需要将左右区间做一个有序排序,这个是怎么做到的。既然是递归我们就不用想了,直接调用MergeSort(nums, low, mid);MergeSort(nums, mid + 1, high); 因为我们的目的是让;MergeSort函数实现对输入的左右区间内元素实现排序,好!那我在整体排序之前需要对左右小区域排序。等这两个递归出来已经排好了。而递归的过程呢又是进去后再细分细分,直到low=high的时候或者high降低到high=low的时候结束跳出,一般这个时候比如low=1,high=2 就会跳出进入一次操作了。然后实现2个元素的排列。最差的一次就是2个元素能排好。然后不断排,排排~
class Solution {
public:
void MergeArray(vector<int>& nums, int low, int high){
vector<int> temp(high - low + 1, 0);
int mid = (low + high)/2, k = 0;
int i = low, m = mid, j = mid + 1, n = high;
while(i <= m && j <= n){
if(nums[i] <= nums[j]) temp[k++] = nums[i++];
else temp[k++] = nums[j++];
}
while(i <= m) temp[k++] = nums[i++];
while(j <= n) temp[k++] = nums[j++];
for(int p = 0; p < temp.size(); p++)
nums[low + p] = temp[p];
}
void MergeSort(vector<int>& nums, int low, int high){
if(low < high){
int mid = (low + high)/2;
MergeSort(nums, low, mid);
MergeSort(nums, mid + 1, high);
MergeArray(nums, low, high);
}
}
void sortIntegers(vector<int>& nums) {
MergeSort(nums, 0, (int)nums.size() -1);
}
};
3.冒泡排序
冒泡排序,顾名思义,就是大的值给你先冒出来。每一趟确定一个元素,一趟扫描采取相邻元素比较的形式进行。时间复杂度O(n2),具体的原理是什么呢?注意到每次比较相邻元素,大的就右移,可以看到最大的元素肯定会不断右移到最右边,所以每次都能确定一个最大元素到数组最右侧。循环nums.size()-1次就能确定所有的元素了!
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
int numsSize = nums.size();
//每一趟确定一个元素,总共numsSize-1趟
for (int i = 1; i < numsSize; ++i){
//每次比较两个相邻元素,出现逆序则交换,就是把大的元素逐渐移动到最后(故名为冒泡)
for (int j = 0; j + 1 < numsSize; ++j){
if (nums[j] > nums[j + 1]){//出现逆序,交换
int tempVal = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tempVal;
}
}
}
return nums;
}
};