【经典算法】快速排序

  与归并排序一样,快速排序使用也使用了分治的思想。下面是对一个典型的子数组A[p,...,r]进行快速排序的三步分治过程:

  分解:数组A[p,...,r]被划分成两个(可能为空)子数组A[P,...,q-1]和A[q+1,...,r],使得A[p,...,q-1]中每个元素都小于等于A[q],而A[q]也小于等于A[q+1,...,r]中的每个元素。其中,计算下标q也是划分过程的一部分。

  解决:通过递归调用快速排序,对子数组啊A[P,...,q-1]和A[q+1,...,r]进行排序。

  合并:因为子数组都是原址排序的,所以不需要合并操作:数组A[P,...r]已经有序。

  下面是程序实现快速排序:

1 void QuickSort(int a[], int p, int r) {
2     if (p < r) {
3         int q = Partition(a, p, r);
4         QuickSort(A, p, q - 1);
5         QuickSort(A, q + 1, r);
6     }
7 }

  数组的划分:

  算法的关键部分是Partition过程,它实现了对子数组A[p,...r]的原址重排。快速排序的分治partition过程有两种方法。

  1) 两个下标分别从首、尾向中间扫描的方法。

  假设每次总是以当前表中第一个元素作为枢纽值(基准)对表进行划分,则必须将表中比枢纽值大的元素向右移动,比枢纽值小的元素向左移动,使得一趟Partition()操作之后,表中的元素被枢纽值一分为二。

 1 int Partition(int a[], int low, int high) {
 2     int pivot = a[low];
 3     while (low < high) {
 4         while (low < high && a[high] >= pivot) --high;
 5         a[low] = a[high];
 6         while (low < high && a[low] <= pivot) ++low;
 7         a[high] = a[low];
 8     }
 9     a[low] = pivot;
10     return low;
11 }

  2)两个指针索引一前一后逐步向后扫描的方法(算法导论)。

 1 int Partition(int a[], int low, int high) {
 2     int pivot = a[high];
 3     int i = low - 1;
 4     for (int j = low; j <= high - 1; j++) {
 5         if (a[low] <= pivot) {
 6             ++i;
 7             swap(a[i], a[j]);
 8         }
 9     }
10     swap(a[i + 1], a[high]);
11     return i+1;
12 }

  注意:上诉算法有一个特点,即一次划分后,枢纽左边的相对位置不变。比如,原始序列:[3,8,7,1,2,5,6,4]->[3,1,2,4,7,5,6,8]。枢纽4左边的相对顺序不变,元素3,1,2保持在初始序列中的相对顺序(原序列中为3,...,1,2,...),某些应用要求序列的一部分保持相对顺序,这时可以考虑此种划分。

  快速排序算法的性能分析如下:

  空间效率:由于快速排序是递归的,需要借助一个递归工作栈来保存每一层递归调用的必要信息,其容量应与递归调用的最大深度一致。最好情况下为⌈log2(n+1)⌉;最坏情况下,因为要进行n-1次递归调用,所以栈的深度为O(n);平均情况下栈的深度为O(log2n)。因而空间复杂度在最坏情况下为O(n),平均情况下为O(log2n)。

  时间效率:快速排序的运行时间与划分是否对称有关,而后者又与具体使用的划分算法有关。快速排序最坏的情况发生在两个区域分别包含n-1个元素和0个元素时,这种程度的不对称性若发生在每一层递归上,即对应于初始排序表基本有序货基本逆序时,就得到最坏情况下的时间复杂度为O(n2)。

   有很多方法可以提高算法的效率。一种方法是当递归过程中划分得到的子序列的规模较小时不要再继续调用快速排序,可以直接采用直接插入排序算法进行后续的排序工作。另一种方法就是尽量选取一个可以将数据中分的枢轴元素。如从序列的头尾以及中间选取三个元素,再取这三个元素的中间值作为最终的枢轴元素(数据结构与算法分析);或者随机从当前列表中选取枢轴元素(算法导论),这样做使得最坏情况在实际安排中几乎不会发生。

  在最理想状态下,也即Partition()可能做到最平衡的划分中,得到的两个子问题的大小都不可能大于n/2,这种情况下,快速排序的运行速度将大大提升,此时,时间复杂度为O(nlog2n)。好在快速排序平均情况下运行时间与其最佳情况下的运行时间很接近,而不是接近最坏情况下的运行时间。

  快速排序是所有内部排序算法中平均性能最优的排序算法。

  稳定性:快速排序不是稳定的排序算法。

一、快速排序一次排序的应用

  1.一个数组中存储有且仅有大写和小写字母,编写一个函数对数组内的字母重新排列,让小写字母在所有大写字母之前。

 1 void Partition(char a[], int length) {
 2     if (a == NULL || length <= 0)
 3         return;
 4
 5     int i = 0;
 6     for (int j = 0; j <= length - 1; ++j) {
 7         if (a[j] >= ‘a‘ && a[j] <= ‘z‘) {
 8             i++;
 9             char temp = a[i];
10             a[i] = a[j];
11             a[j] = temp;
12         }
13     }
14 }

  2. 给定含有n个元素的整形数组a, 其中包括0元素和非0元素,对数组进行排序,要求:

    1)排序后所有0元素在前,所有非零元素在后,且非零元素排序前后相对位置不变。

    2)不能使用额外存储空间。、

    例如:

      输入 0、3、0、2、1、0、0

      输出 0、0、0、0、3、2、1

    解答:此处要求非零元素排序前后相对位置不变,可以利用快排一次排序的第二种情况。

void Partition(int A[], int p, int r) {
    int i = r + 1;
    for (int j = r; j >= p; --j) {
        if (A[j] != 0) {
            --i;
            int temp = A[i];
            A[i] = A[j];
            A[j] = temp;
        }
    }
}

  3. 荷兰国旗问题

    将乱序的红白蓝三色小球排列成同颜色在一起的小球组(按照红白蓝排序),这个问题称为荷兰过期问题。这是因为我们可以将红白蓝小球想象成为条状物,有序排列后正好组成荷兰国旗,用0表示红球,2为篮球,1为白球。

    解答:这个问题,类似于快排中partition过程。不过,要用三个指针,一个begin,一中current,一后end,begin与current都初始化指向数组首部,end初始化指向数组尾部。

    1. current遍历整个数组序列,current指1时,不交换,current++;

    2. current指0时,与begin交换,而后current++,begin++;

    3. current指2时,与end交换,而后,current不动,end--。

 1 while (current <= end) {
 2     if (array[current] == 0) {
 3         swap(array[current], array[begin]);
 4         current++;
 5         begin++;
 6     } else if (array[current] == 1) {
 7         current++;
 8     } else {
 9         swap(array[current], array[end]);
10         end--;
11     }
12 }

二、最小的k个数

  输入n个整数,输出其中最小的k个。

  例如输入1,2,3,4,5,6,7,8这8个数字,则最小的4个数字为1,2,3,4

  解答:分析:这到底最简单的思路莫过于把输入的n个整数排序,这样排在最前面的k个数就是最小的k个数。只是这种思路的时间复杂度为O(nlgn)。我们试着寻找更快的解题思路。

  我们设最小的k个数中最大的数为A。在快速排序算法中,我们现在数组中随机选择一个数字,然后调整数组中数字的顺序,使得比选中的数字小的数字都排在他的左边,比选中的数字大的数字都排在它的右边(即快排一次排序)。如果这个选中的数字的下标刚好是k-1(下标从0)开始,那么这个数字(就是A)加上左侧的k-1个数字就是最小的k个数。

  如果它的下标大于k-1,那么A应该位于它的左边,我们可以接着在它的右边部分的数组中寻找。可见这是一个递归问题,但是注意我们找到的k个数不一定是有序的。

 1 int Partition(int a[], int p, int r) {
 2     int pivot = a[r];
 3     int i = p - 1;
 4     for (int j = p; j <= r - 1; ++j) {
 5         if (a[j] <= pivot) {
 6             ++i;
 7             swap(a[i], a[j]);
 8         }
 9     }
10     swap(a[i + 1], a[r]);
11     return i + 1;
12 }
13 void GetLeastKNum(int *input, int n, int k) {
14     if (input == NULL || n <= 0 || k > n || k <= 0)
15         return;
16
17     int start = 0;
18     int end = n - 1;
19     int index = Partition(input, start, end);
20     while (index != k - 1) {
21         if (index < k - 1) {
22         start = index + 1;
23         index = Partition(input, start, end);
24         } else {
25             end = index - 1;
26             index = Partition(input, start, end);
27         }
28     }
29
30     for (int i = 0; i <= k - 1; ++i) {
31         cout << input[i];
32     }
33     cout << endl;
34 }

  上述方法的时间复杂度是O(n)。

时间: 2024-11-05 12:26:30

【经典算法】快速排序的相关文章

[经典算法] 快速排序

题目说明: 快速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序 n 个项目要Ο(n log n)次比较.在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见.事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来. 题目解析: 快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists). 算法步骤: 1.从数列中挑出一个元素,称为

经典算法——快速排序

快速排序的基本思想(分治法): 1.先从数列中取出一个数作为基准数 2.分区过程:将比基准数大的放在其右边,小的放在其左边 3.再对左右区间重复分区操作,直到各区间只有一个数 快排Java代码 package quicksorttest; import java.util.Scanner; public class QuicksortTest { static int[] array = new int[10]; public static void main(String[] args) {

经典算法系列一-快速排序

写一个系列的经典算法,共同学习! 1.快速排序 基本思想: 1.先从数列中取出一个数作为基准数. 2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边. 3.再对左右区间重复第二步,直到各区间只有一个数. 在网上看到一篇博客,把快速排序命名为 挖坑填数+分治法,感觉很有道理,所以等会程序就按这两部来写. 先说说挖坑填数的思想: 1.i=L;j=R; 将基数挖出形成第一个坑a[i]. 2.j--由后向前找出比它的小的数,找到后挖出此数填前一个坑a[i]. 3.i++由前向后

经典排序算法 - 快速排序Quick sort

经典排序算法 - 快速排序Quick sort 原理,通过一趟扫描将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列 举个例子 如无序数组[6 2 4 1 5 9] a),先把第一项[6]取出来, 用[6]依次与其余项进行比较, 如果比[6]小就放[6]前边,2 4 1 5都比[6]小,所以全部放到[6]前边 如果比[6]大就放[6]后边,9比[6]大,放到[6

【从零学习经典算法系列】分治策略实例——快速排序(QuickSort)

在前面的博文(http://blog.csdn.net/jasonding1354/article/details/37736555)中介绍了作为分治策略的经典实例,即归并排序,并给出了递归形式和循环形式的c代码实例.但是归并排序有两个特点,一是在归并(即分治策略中的合并步骤)上花费的功夫较多,二是排序过程中需要使用额外的存储空间(异地排序算法<out of place sort>). 为了节省存储空间,出现了快速排序算法(原地排序in-place sort).快速排序是由东尼·霍尔所发展的一

动态展示十大经典算法

算法一:快速排序算法 快速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序n个项目要Ο(nlogn)次比较.在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见.事实上,快速排序通常明显比其他Ο(nlogn)算法更快,因为它的内部循环(innerloop)可以在大部分的架构上很有效率地被实现出来. 快速排序使用分治法(Divideandconquer)策略来把一个串行(list)分为两个子串行(sub-lists). 算法步骤: 1.从数列中挑出一个元素,称为“基准”(pivot),

【经典算法大全】收集51种经典算法 初学者必备

<经典算法大全>是一款IOS平台的应用.里面收录了51种常用算法,都是一些基础问题.博主觊觎了好久,可悲哀的是博主没有苹果,所以从网上下了老奔的整理版并且每个都手敲了一遍. 虽然网上也有博客贴了出来,但是自己写写感觉总是好的.现在分享个大家. 代码和运行结果难免有出错的地方,请大家多多包涵. 1.河内之塔(汉诺塔) 2.费式数列 3.巴斯卡三角形 4.三色棋 5.老鼠走迷宫(1) 6.老鼠走迷宫(2) 7.骑士走棋盘 8.八皇后 9.八枚银币 10.生命游戏 11.字串核对 12.双色河内塔,

编程算法 - 快速排序算法 代码(C)

快速排序算法 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 经典的快速排序算法, 作为一个编程者, 任何时候都要完整的手写. 代码: /* * main.cpp * * Created on: 2014.6.12 * Author: Spike */ /*eclipse cdt, gcc 4.8.1*/ #include <stdio.h> #include <stdlib.h> int RandomInRange(int min,

经典算法宝典——分治思想(四)(1)

分治法(Divide and Conquer)的设计思想是,将一个难以直接解决的大问题,分割成一些规模较小的几个相似问题,以便各个击破,分而治之. 说明: 分治策略的应用很广,具体表现形式各异,比如:折半查找.合并排序.快速排序.二叉树遍历(先遍历左子树再遍历右子树).二叉排序树的查找等算法. 一.分治算法框架 1.算法设计思想 分治法求解问题的过程是,将整个问题分解成若干个小问题后分而治之.如果分解得到的子问题相对来说还太大,则可反复使用分治策略将这些子问题分成更小的同类型子问题,直至产生出方

经典算法大全

原文地址:经典算法大全 作者:liurhyme 经                                                                    典                                                                    算                                                                    法