【每日算法】交换排序算法之快速排序
2017-04-02 23:04
176 查看
恩,重头戏开始了,快速排序是各种笔试面试最爱考的排序算法之一,且排序思想在很多算法题里面被广泛使用。是需要重点掌握的排序算法。
步骤为:
1、从数列中挑出一个元素,称为 "基准"(pivot),
2、重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3、递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
算法伪代码描述:
在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
最差时间复杂度: O(n^2)
最优时间复杂度: O(n log n)
平均时间复杂度: O(n log n)
最差空间复杂度: 根据实现的方式不同而不同
快速排序会递归地进行很多轮,其中每一轮称之为快排的partition算法,即上述算法描述中的第2步,非常重要,且在各种笔试面试中用到该思想的算法题层出不穷,下图为第一轮的partition算法的一个示例。
![](http://img.blog.csdn.net/20130929152345453?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaGFuX3hpYW95YW5n/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
Flash:
可一步步参见http://ds.fzu.edu.cn/fine/resources/FlashContent.asp?id=86中的快排过程
视频 舞动的排序算法
http://v.youku.com/v_show/id_XMzMyODk4NTQ4.html
1、版本一
我们选取数组的第一个元素作为主元,每一轮都是和第一个元素比较大小,通过交换,分成大于和小于它的前后两部分,再递归处理。
代码如下
2、版本二
随机选基准数的快排
例题1、最小的k个数,输入n个整数,找出其中最下的k个数,例如输入4、5、1、6、2、7、3、8、1、2,输出最下的4个数,则输出1、1、2、2。
当然,博主也知道这题可以建大小为k的大顶堆,然后用堆的方法解决。
但是这个题目可也以仿照快速排序,运用partition函数进行求解,不过我们完整的快速排序分割后要递归地对前后两段继续进行分割,而这里我们需要做的是判定分割的位置,然后再确定对前段还是后段进行分割,所以只对单侧分割即可。代码如下:
例题2、判断数组中出现超过一半的数字
当然,这道题很多人都见过,而且最通用的一种解法是数对对消的思路。这里只是再给大家提供一种思路,快排partition的方法在很多地方都能使用,比如这题。我们也可以选择合适的判定条件进行递归。代码如下:
例题3、有一个由大小写组成的字符串,现在需要对他进行修改,将其中的所有小写字母排在大写字母的前面(不要求保持原顺序)
这题可能大家都能想到的方法是:设置首尾两个指针,首指针向后移动寻找大写字母,尾指针向前移动需找小写字母,找到后都停下,交换。之后继续移动,直至相遇。这种方法在这里我就不做讨论写代码了。
但是这题也可以采用类似快排的partition。这里使用从左往后扫描的方式。字符串在调整的过程中可以分成两个部分:已排好的小写字母部分、待调整的剩余部分。用两个指针i和j,其中i指向待调整的剩余部分的第一个元素,用j指针遍历待调整的部分。当j指向一个小写字母时,交换i和j所指的元素。向前移动i、j,直到字符串末尾。代码如下:
1)算法简介
快速排序是由东尼·霍尔所发展的一种排序算法。其基本思想是基本思想是,通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。2)算法描述和分析
快速排序使用分治法来把一个串(list)分为两个子串行(sub-lists)。步骤为:
1、从数列中挑出一个元素,称为 "基准"(pivot),
2、重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3、递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
算法伪代码描述:
function quicksort(q) var list less, pivotList, greater if length(q) ≤ 1 { return q } else { select a pivot value pivot from q for each x in q except the pivot element if x < pivot then add x to less if x ≥ pivot then add x to greater add pivot to pivotList return concatenate(quicksort(less), pivotList, quicksort(greater)) }
在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
最差时间复杂度: O(n^2)
最优时间复杂度: O(n log n)
平均时间复杂度: O(n log n)
最差空间复杂度: 根据实现的方式不同而不同
3)算法图解、flash演示、视频演示
图解:快速排序会递归地进行很多轮,其中每一轮称之为快排的partition算法,即上述算法描述中的第2步,非常重要,且在各种笔试面试中用到该思想的算法题层出不穷,下图为第一轮的partition算法的一个示例。
Flash:
可一步步参见http://ds.fzu.edu.cn/fine/resources/FlashContent.asp?id=86中的快排过程
视频 舞动的排序算法
http://v.youku.com/v_show/id_XMzMyODk4NTQ4.html
4)算法代码
事实上,这个地方需要提一下的是,快排有很多种版本。例如,我们“基准数”的选择方法不同就有不同的版本,但重要的是快排的思想,我们熟练掌握一种版本,在最后的笔试面试中也够用了,我这里罗列几种最有名的版本C代码。
1、版本一
我们选取数组的第一个元素作为主元,每一轮都是和第一个元素比较大小,通过交换,分成大于和小于它的前后两部分,再递归处理。
代码如下
/************************************************** 函数功能:对数组快速排序 函数参数:指向整型数组arr的首指针arr; 整型变量left和right左右边界的下标 函数返回值:空 /**************************************************/ void QuickSort(int *arr, int left, int right) { int i,j; if(left<right) { i=left;j=right; arr[0]=arr[i]; //准备以本次最左边的元素值为标准进行划分,先保存其值 do { while(arr[j]>arr[0] && i<j) j--; //从右向左找第1个小于标准值的位置j if(i<j) //找到了,位置为j { arr[i] = arr[j]; i++; } //将第j个元素置于左端并重置i while(arr[i]<arr[0] && i<j) i++; //从左向右找第1个大于标准值的位置i if(i<j) //找到了,位置为i { arr[j] = arr[i]; j--; } //将第i个元素置于右端并重置j }while(i!=j); arr[i] = arr[0]; //将标准值放入它的最终位置,本次划分结束 quicksort(arr, left, i-1); //对标准值左半部递归调用本函数 quicksort(arr, i+1, right); //对标准值右半部递归调用本函数 } }
2、版本二
随机选基准数的快排
//使用引用,完成两数交换 void Swap(int& a , int& b) { int temp = a; a = b; b = temp; } //取区间内随机数的函数 int Rand(int low, int high) { int size = hgh - low + 1; return low + rand()%size; } //快排的partition算法,这里的基准数是随机选取的 int RandPartition(int* data, int low , int high) { swap(data[rand(low,high)], data[low]);// int key = data[low]; int i = low; for(int j=low+1; j<=high; j++) { if(data[j]<=key) { i = i+1; swap(data[i], data[j]); } } swap(data[i],data[low]); return i; } //递归完成快速排序 void QuickSort(int* data, int low, int high) { if(low<high) { int k = RandPartition(data,low,high); QuickSort(data,low,k-1); QuickSort(data,k+1,high); } }
5)考察点,重点和频度分析
完全考察快排算法本身的题目,多出现在选择填空,基本是关于时间空间复杂度的讨论,最好最坏的情形交换次数等等。倒是快排的partition算法需要特别注意!频度极高地被使用在各种算法大题中!详见下小节列举的面试小题。6)笔试面试例题
这里要重点强调的是快排的partition算法,博主当年面试的时候就遇到过数道用该思路的算法题,举几道如下:例题1、最小的k个数,输入n个整数,找出其中最下的k个数,例如输入4、5、1、6、2、7、3、8、1、2,输出最下的4个数,则输出1、1、2、2。
当然,博主也知道这题可以建大小为k的大顶堆,然后用堆的方法解决。
但是这个题目可也以仿照快速排序,运用partition函数进行求解,不过我们完整的快速排序分割后要递归地对前后两段继续进行分割,而这里我们需要做的是判定分割的位置,然后再确定对前段还是后段进行分割,所以只对单侧分割即可。代码如下:
void GetLeastNumbers_by_partition(int* input, int n, int* output, int k) { if(input == NULL || output == NULL || k > n || n <= 0 || k <= 0) return; int start = 0; int end = n - 1; int index = Partition(input, n, start, end); while(index != k - 1) { if(index > k - 1) { end = index - 1; index = Partition(input, n, start, end); } else { start = index + 1; index = Partition(input, n, start, end); } } for(int i = 0; i < k; ++i) output[i] = input[i]; }
例题2、判断数组中出现超过一半的数字
当然,这道题很多人都见过,而且最通用的一种解法是数对对消的思路。这里只是再给大家提供一种思路,快排partition的方法在很多地方都能使用,比如这题。我们也可以选择合适的判定条件进行递归。代码如下:
bool g_bInputInvalid = false; bool CheckInvalidArray(int* numbers, int length) { g_bInputInvalid = false; if(numbers == NULL && length <= 0) g_bInputInvalid = true; return g_bInputInvalid; } bool CheckMoreThanHalf(int* numbers, int length, int number) { int times = 0; for(int i = 0; i < length; ++i) { if(numbers[i] == number) times++; } bool isMoreThanHalf = true; if(times * 2 <= length) { g_bInputInvalid = true; isMoreThanHalf = false; } return isMoreThanHalf; } int MoreThanHalfNum_Solution1(int* numbers, int length) { if(CheckInvalidArray(numbers, length)) return 0; int middle = length >> 1; int start = 0; int end = length - 1; int index = Partition(numbers, length, start, end); while(index != middle) { if(index > middle) { end = index - 1; index = Partition(numbers, length, start, end); } else { start = index + 1; index = Partition(numbers, length, start, end); } } int result = numbers[middle]; if(!CheckMoreThanHalf(numbers, length, result)) result = 0; return result; }
例题3、有一个由大小写组成的字符串,现在需要对他进行修改,将其中的所有小写字母排在大写字母的前面(不要求保持原顺序)
这题可能大家都能想到的方法是:设置首尾两个指针,首指针向后移动寻找大写字母,尾指针向前移动需找小写字母,找到后都停下,交换。之后继续移动,直至相遇。这种方法在这里我就不做讨论写代码了。
但是这题也可以采用类似快排的partition。这里使用从左往后扫描的方式。字符串在调整的过程中可以分成两个部分:已排好的小写字母部分、待调整的剩余部分。用两个指针i和j,其中i指向待调整的剩余部分的第一个元素,用j指针遍历待调整的部分。当j指向一个小写字母时,交换i和j所指的元素。向前移动i、j,直到字符串末尾。代码如下:
#include <iostream> using namespace std; void Proc( char *str ) { int i = 0; int j = 0; //移动指针i, 使其指向第一个大写字母 while( str[i] != '\0' && str[i] >= 'a' && str[i] <= 'z' ) i++; if( str[i] != '\0' ) { //指针j遍历未处理的部分,找到第一个小写字母 for( j=i; str[j] != '\0'; j++ ) { if( str[j] >= 'a' && str[j] <= 'z' ) { char tmp = str[i]; str[i] = str[j]; str[j] = tmp; i++; } } } } int main() { char data[] = "SONGjianGoodBest"; Proc( data ); return 0; }
相关文章推荐
- 第十五周——项目一—验证算法(4)交换排序之快速排序
- java实现排序算法之交换排序(冒泡排序和快速排序)
- 【每日算法】交换排序算法之鸡尾酒排序/双向冒泡排序
- 交换排序算法---冒泡排序与快速排序
- 【每日算法】交换排序算法之冒泡排序
- 【算法之家】——交换排序之冒泡排序与快速排序
- 算法 排序算法之交换排序--冒泡排序和快速排序
- 算法:交换排序之快速排序
- 有意思的排序算法-快速排序
- 交换排序之快速排序(java实现)
- 交换排序之快速排序
- 【数据结构】排序算法(二)之交换排序之快速排序(QuickSort)
- 有意思的排序算法-快速排序
- 数据结构&算法实践—【排序|交换排序】奇偶排序
- 数据结构&算法实践—【排序|交换排序】冒泡排序及改进
- 数据结构&算法实践—【排序|交换排序】冒泡排序及改进
- 【数据结构】排序算法(二)之交换排序之快速排序(QuickSort)
- 每日一算法之快速排序原理及实现
- 白话经典算法系列之四 直接选择排序及交换二个数据的正确实现
- 数据结构&算法实践—【排序|交换排序】鸡尾酒排序