漫画:什么是快速排序?(完整版)

————— 第二天 —————

————————————

同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。

不同的是,冒泡排序在每一轮只把一个元素冒泡到数列的一端,而快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。

这种思路就叫做分治法

每次把数列分成两部分,究竟有什么好处呢?

假如给定8个元素的数列,一般情况下冒泡排序需要比较8轮,每轮把一个元素移动到数列一端,时间复杂度是O(n^2)。

而快速排序的流程是什么样子呢?

如图所示,在分治法的思想下,原数列在每一轮被拆分成两部分,每一部分在下一轮又分别被拆分成两部分,直到不可再分为止。

这样一共需要多少轮呢?平均情况下需要logn轮,因此快速排序算法的平均时间复杂度是 O(nlogn)

基准元素的选择

基准元素,英文pivot,用于在分治过程中以此为中心,把其他元素移动到基准元素的左右两边。

那么基准元素如何选择呢?

最简单的方式是选择数列的第一个元素:

这种选择在绝大多数情况是没有问题的。但是,假如有一个原本逆序的数列,期望排序成顺序数列,那么会出现什么情况呢?

..........

我们该怎么避免这种情况发生呢?

其实很简单,我们可以不选择数列的第一个元素,而是随机选择一个元素作为基准元素

这样一来,即使在数列完全逆序的情况下,也可以有效地将数列分成两部分。

当然,即使是随机选择基准元素,每一次也有极小的几率选到数列的最大值或最小值,同样会影响到分治的效果。

所以,快速排序的平均时间复杂度是 O(nlogn),最坏情况下的时间复杂度是 O(n^2)

元素的移动

选定了基准元素以后,我们要做的就是把其他元素当中小于基准元素的都移动到基准元素一边,大于基准元素的都移动到基准元素另一边。

具体如何实现呢?有两种方法:

1.挖坑法

2.指针交换法

何谓挖坑法?我们来看一看详细过程。

给定原始数列如下,要求从小到大排序:

首先,我们选定基准元素Pivot,并记住这个位置index,这个位置相当于一个“坑”。并且设置两个指针left和right,指向数列的最左和最右两个元素:

接下来,从right指针开始,把指针所指向的元素和基准元素做比较。如果比pivot大,则right指针向左移动;如果比pivot小,则把right所指向的元素填入坑中。

在当前数列中,1<4,所以把1填入基准元素所在位置,也就是坑的位置。这时候,元素1本来所在的位置成为了新的坑。同时,left向右移动一位。

此时,left左边绿色的区域代表着小于基准元素的区域。

接下来,我们切换到left指针进行比较。如果left指向的元素小于pivot,则left指针向右移动;如果元素大于pivot,则把left指向的元素填入坑中。

在当前数列中,7>4,所以把7填入index的位置。这时候元素7本来的位置成为了新的坑。同时,right向左移动一位。

此时,right右边橙色的区域代表着大于基准元素的区域。

下面按照刚才的思路继续排序:

8>4,元素位置不变,right左移

2<4,用2来填坑,left右移,切换到left。

6>4,用6来填坑,right左移,切换到right。

3<4,用3来填坑,left右移,切换到left。

5>4,用5来填坑,right右移。这时候left和right重合在了同一位置。

这时候,把之前的pivot元素,也就是4放到index的位置。此时数列左边的元素都小于4,数列右边的元素都大于4,这一轮交换终告结束。

public class QuickSort {

public static void quickSort(int[] arr, int startIndex, int endIndex) {

// 递归结束条件:startIndex大等于endIndex的时候

if (startIndex >= endIndex) {

return;

}

// 得到基准元素位置

int pivotIndex = partition(arr, startIndex, endIndex);

// 用分治法递归数列的两部分

quickSort(arr, startIndex, pivotIndex - 1);

quickSort(arr, pivotIndex + 1, endIndex);

}

private static int partition(int[] arr, int startIndex, int endIndex) {

// 取第一个位置的元素作为基准元素

int pivot = arr[startIndex];

int left = startIndex;

int right = endIndex;

// 坑的位置,初始等于pivot的位置

int index = startIndex;

//大循环在左右指针重合或者交错时结束

while ( right >= left ){

//right指针从右向左进行比较

while ( right >= left ) {

if (arr[right] < pivot) {

arr[left] = arr[right];

index = right;

left++;

break;

}

right--;

}

//left指针从左向右进行比较

while ( right >= left ) {

if (arr[left] > pivot) {

arr[right] = arr[left];

index = left;

right--;

break;

}

left++;

}

}

arr[index] = pivot;

return index;

}

public static void main(String[] args) {

int[] arr = new int[] {4,7,6,5,3,2,8,1};

quickSort(arr, 0, arr.length-1);

System.out.println(Arrays.toString(arr));

}

}

代码中,quickSort方法通过递归的方式,实现了分而治之的思想。

partition方法则实现元素的移动,让数列中的元素依据自身大小,分别移动到基准元素的左右两边。在这里,我们使用移动方式是挖坑法。

指针交换法

何谓指针交换法?我们来看一看详细过程。

给定原始数列如下,要求从小到大排序:

开局和挖坑法相似,我们首先选定基准元素Pivot,并且设置两个指针left和right,指向数列的最左和最右两个元素:

接下来是第一次循环,从right指针开始,把指针所指向的元素和基准元素做比较。如果大于等于pivot,则指针向移动;如果小于pivot,则right指针停止移动,切换到left指针。

在当前数列中,1<4,所以right直接停止移动,换到left指针,进行下一步行动。

轮到left指针行动,把指针所指向的元素和基准元素做比较。如果小于等于pivot,则指针向移动;如果大于pivot,则left指针停止移动。

由于left一开始指向的是基准元素,判断肯定相等,所以left右移一位。

由于7 > 4,left指针在元素7的位置停下。这时候,我们让left和right指向的元素进行交换

接下来,我们进入第二次循环,重新切换到right向左移动。right先移动到8,8>2,继续左移。由于2<8,停止在2的位置。

切换到left,6>4,停止在6的位置。

元素6和2交换。

进入第三次循环,right移动到元素3停止,left移动到元素5停止。

元素5和3交换。

进入第四次循环,right移动到元素3停止,这时候请注意,left和right指针已经重合在了一起。

当left和right指针重合之时,我们让pivot元素和left与right重合点的元素进行交换。此时数列左边的元素都小于4,数列右边的元素都大于4,这一轮交换终告结束。

public class QuickSort {

public static void quickSort(int[] arr, int startIndex, int endIndex) {

// 递归结束条件:startIndex大等于endIndex的时候

if (startIndex >= endIndex) {

return;

}

// 得到基准元素位置

int pivotIndex = partition(arr, startIndex, endIndex);

// 根据基准元素,分成两部分递归排序

quickSort(arr, startIndex, pivotIndex - 1);

quickSort(arr, pivotIndex + 1, endIndex);

}

private static int partition(int[] arr, int startIndex, int endIndex) {

// 取第一个位置的元素作为基准元素

int pivot = arr[startIndex];

int left = startIndex;

int right = endIndex;

while( left != right) {

//控制right指针比较并左移

while(left<right && arr[right] > pivot){

right--;

}

//控制right指针比较并右移

while( left<right && arr[left] <= pivot) {

left++;

}

//交换left和right指向的元素

if(left<right) {

int p = arr[left];

arr[left] = arr[right];

arr[right] = p;

}

}

//pivot和指针重合点交换

int p = arr[left];

arr[left] = arr[startIndex];

arr[startIndex] = p;

return left;

}

public static void main(String[] args) {

int[] arr = new int[] {4,7,6,5,3,2,8,1};

quickSort(arr, 0, arr.length-1);

System.out.println(Arrays.toString(arr));

}

}

和挖坑法相比,指针交换法在partition方法中进行的元素交换次数更少。

非递归实现

为什么这样说呢?

因为我们代码中一层一层的方法调用,本身就是一个函数栈。每次进入一个新方法,就相当于入栈;每次有方法返回,就相当于出栈。

所以,我们可以把原本的递归实现转化成一个栈的实现,在栈当中存储每一次方法调用的参数:

下面我们来看一下代码:

public class QuickSortWithStack {

public static void quickSort(int[] arr, int startIndex, int endIndex) {

// 用一个集合栈来代替递归的函数栈

Stack<Map<String, Integer>> quickSortStack = new Stack<Map<String, Integer>>();

// 整个数列的起止下标,以哈希的形式入栈

Map rootParam = new HashMap();

rootParam.put("startIndex", startIndex);

rootParam.put("endIndex", endIndex);

quickSortStack.push(rootParam);

// 循环结束条件:栈为空时结束

while (!quickSortStack.isEmpty()) {

// 栈顶元素出栈,得到起止下标

Map<String, Integer> param = quickSortStack.pop();

// 得到基准元素位置

int pivotIndex = partition(arr, param.get("startIndex"), param.get("endIndex"));

// 根据基准元素分成两部分, 把每一部分的起止下标入栈

if(param.get("startIndex") < pivotIndex -1){

Map<String, Integer> leftParam = new HashMap<String, Integer>();

leftParam.put("startIndex", param.get("startIndex"));

leftParam.put("endIndex", pivotIndex -1);

quickSortStack.push(leftParam);

}

if(pivotIndex + 1 < param.get("endIndex")){

Map<String, Integer> rightParam = new HashMap<String, Integer>();

rightParam.put("startIndex", pivotIndex + 1);

rightParam.put("endIndex", param.get("endIndex"));

quickSortStack.push(rightParam);

}

}

}

private static int partition(int[] arr, int startIndex, int endIndex) {

// 取第一个位置的元素作为基准元素

int pivot = arr[startIndex];

int left = startIndex;

int right = endIndex;

while( left != right) {

//控制right指针比较并左移

while(left<right && arr[right] > pivot){

right--;

}

//控制right指针比较并右移

while( left<right && arr[left] <= pivot) {

left++;

}

//交换left和right指向的元素

if(left<right) {

int p = arr[left];

arr[left] = arr[right];

arr[right] = p;

}

}

//pivot和指针重合点交换

int p = arr[left];

arr[left] = arr[startIndex];

arr[startIndex] = p;

return left;

}

public static void main(String[] args) {

int[] arr = new int[] {4,7,6,5,3,2,8,1};

quickSort(arr, 0, arr.length-1);

System.out.println(Arrays.toString(arr));

}

}

和刚才的递归实现相比,代码的变动仅仅在quickSort方法当中。该方法中引入了一个存储Map类型元素的栈,用于存储每一次交换时的起始下标和结束下标。

每一次循环,都会让栈顶元素出栈,进行排序,并且按照基准元素的位置分成左右两部分,左右两部分再分别入栈。当栈为空时,说明排序已经完毕,退出循环。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容