白话讲排序系列(三) 快速排序

本文通过大白话讲解快速排序的实现原理,并提供Java版本的完整代码。快速排序属于交换类排序,利用分治思想,平均时间复杂度为O(nlogn)。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

同冒泡排序一样,快速排序属于交换类排序,但是,因为加入了分而治之的思想,快速排序的平均复杂度可以达到O(nlogn);本文将用大白话讲述一下快速排序的实现原理;同样,末尾将附上完整可行的快速排序算法代码(Java版本)。

注:文中有些图可能来自于其他链接,会在文末附上。

为了方便介绍快速排序的整体思想,这里先定义一个数组:

int[] array = new int[] { 45, 38, 65, 97, 76, 13, 27, 49, 20, 54 };

接下来的数据分析,都以该数组为基础:

一个基本概念:基数。

什么是基数呢?就是我们从待排序的数列中,随机选出一个数,就可以称为基数,它没什么特殊的含义,就是完全随机挑出来的一个数,我们只是称其为基数;通常呢,我们会挑选第一个数字作为基数,对于讨论的数组,就是45,其作为基数。

好地,接下来真正进入快速排序的流程,全是大白话,绝对通俗易懂:

当前:数组元素为:45 38 65 97 76 13 27 49 20 54;10个数字,前文已经选定了基数为45。

第一步,我们把45单独挖出来,其所在的位置,就形成了一个坑(__符号表示此处是坑,没有萝卜(数字));挖掉之后的效果如下:注:上面一行是索引值;这里以0为开始索引。

0   1    2   3  4   5  6   7   8   9

__ 38 65 97 76 13 27 49 20 54

第二步,从右侧开始遍历,寻找第一个比基数,就是我们定义的那个基数45小的数字,我们从右向左走,找到的数字是20,这是第一个遇到的比45小的数字,接着,将其挖掉,则原地就留下了另一个坑,这时候,效果如下:

0    1     2     3    4    5    6    7    8    9

__ 38  65  97  76  13 27  49  __  54    :注意,目前图中有了两个深坑。

第三步,把拿出来的20放入到原先45所在的位置,实质就是从右侧找到第一个小于基数的数字,与基数交换,这一步执行后的结果如下:

 0    1    2     3     4    5    6    7     8    9

20 38  65  97  76  13  27  49  __  54  :注意,目前45,也就是基数,还未放入坑。

第四步,从左侧开始遍历,寻找第一个比基数45更大的数,找到的数字是65,然后把该数字放入右边空着的坑里;执行结果如下:

 0    1    2      3     4     5    6     7      8    9

20 38  __   97  76   13  27  49   65  54 :注意,这时候65的位置留下了一个坑;这时候,45依旧保持悬空状态,没有放坑里;此时左侧索引到达的位置为2;即原先65所在的位置

第五步,再次从右侧开始遍历,注意:这时候要以第二步骤为基础,因为第二步已经遍历到了倒数第二个位置;此次要在此基础上,继续往左走:然后找到了数字27;把该数字拿出来,放入第四步的坑里:执行结果如下:

0     1     2      3    4     5     6    7     8     9

20 38  27   97  76   13  __  49   65  54  :注意,此次操作是在前面操作的基础之上的,逻辑跟第二步是一样的,但是在第二步基础上后续操作;此时,右侧索引到达的位置为6,即原先27所在的位置。

第六步:第二次右侧遍历操作结束,现在开始第二次左侧遍历操作,按照第四步的逻辑,找到了数字97;这里,也是在第四步操作基础上执行的,把97挖出来,然后放在第五步形成的坑里;执行结果如下:

 0    1     2     3     4     5    6    7      8     9

20 38  27   __  76   13  97  49   65  54  :注意,这时候左侧索引所在的位置为3

第七步:第三次右侧遍历,记住,每次遍历的操作都是从右边找小于基数的元素,从左边找大于基数的元素,而且操作都是在原先遍历操作后的位置继续操作的;然后找到了13,放到第六步形成的坑里:执行结果如下:

 0    1     2      3    4     5    6    7       8     9

20 38  27   13  76   __  97  49   65  54 :注意,这时候右侧遍历到了索引为5;左侧遍历到的位置为3,两个索引还未曾相遇,所以可以继续遍历,如果索引相遇,则代表左边全部是小于45的数字,右边全部是大于45的数字,则遍历停止了

第八步:第三次左侧遍历,发现了大于45的数字,76,这时候,左侧索引为4,还未曾碰到右边的索引,所以可以继续进行,把76挖出来,放在第七步的坑里:执行结果如下:

0     1     2     3    4     5    6     7      8     9

20 38  27   13  __   76  97  49   65  54 :注意,这时候左侧索引为4;而右侧索引为5

第九步:程序继续执行,开始第四次右侧遍历,结果,右侧索引为4,左侧索引同样为4,二者相遇了,代表这一次执行结束,而此时,数组中仍然有一个坑,这个坑,就放入我们选定的基数:45。

到此,第一趟排序完成。

快速排序,没有想象中的那么难,这第一趟排序,轻轻松松就完成了,接下来,要总结下这趟排序的要点,因为我们还要把这部分逻辑递归调用,提高快速排序的效率:

  1. 快速排序的实质:首先挖坑,然后遍历填坑;我们先取出一个基数,无论这个基数的位置在哪儿,拿出来之后就形成一个坑;然后遍历从右侧开始,寻找比基数更小的数,挖出来(留出一个坑),填到原先的坑里;然后从左侧寻找比基数大的数,填到上一步的坑里;这就是主体逻辑
  2. 仔细想一下第一步达到的结果,那就是最后整个数列以基数为标准,划分成了两部分?是不是呢,仔细想一下,右边小于基数的数字,现在都放到了左边;而左边大于基数的数字,都被放到了右边,达成了我们想要的效果。
  3. 执行的过程中,必须注意左右的索引是否相遇了,只有左右索引未曾相遇,才能继续执行;相遇了,一次循环就终止了。

好,现在,最复杂的一趟讲完了,然后呢?

前文说了,整个队列已经划分为两个部分了,然后,就是针对这两部分,再度调用快速排序算法,如此反复,很快,整个算法就执行完毕,而整个数列,也就变成完全有序的了。

下面是完整代码(Java版本):内有详细注释:

public class QuickSorting {
	public static void main(String[] args) {
		int[] array = new int[] { 45, 38, 65, 97, 76, 13, 27, 49, 20, 54,47 };
		sort(array, 0, array.length - 1);
		for (int ele : array)
			System.out.print(ele + "  ");
	}

	/**
	 * @param a
	 * @param start
	 * @param end
	 */
	public static void sort(int[] a, int start, int end) {
		// 只有可以继续划分的条件下,才会继续排序操作
		if (start < end) {
			int partition = quickSort(start, end, a);
			sort(a, start, partition - 1);
			sort(a, partition + 1, end);
		}

	}

	/**
	 * @param begin
	 * @param end
	 * @param array
	 * @return 返回值为此次基准值所在的索引位置
	 */
	public static int quickSort(int begin, int end, int[] array) {
		// 选择基数
		int base = array[begin];
		// begin和end就是作为索引的,继续循环的条件就是二者不得相遇
		while (begin < end) {
			// 从右侧开始遍历;如果元素大于基数,则继续
			// 同时需要继续保证begin<end
			// 因为在内部执行过程中,可能会出现begin>=end的情况,需要限制
			while (array[end] > base && begin < end) {
				end--;
			}
			// 因为左侧的元素已经挖出来,留个坑了,直接进行填坑,即赋值
			array[begin] = array[end];
			// 从左侧开始遍历,如果元素小于基数,则继续
			while (array[begin] < base && begin < end) {
				begin++;
			}
			// 否则,把元素放入右侧的坑里
			array[end] = array[begin];
		}
		// 这时候,begin=end,整个数列划分为两部分了
		// 进行赋值操作
		array[begin] = base;
		// 返回begin位置的索引,在接下来的循环中,用于继续划分数据,进行递归排序
		return begin;
	}
}
复杂度分析:
  1. 时间复杂度:O(nlogn);在最糟糕的情况下,能够达到O(n2);平均情况下,复杂度为O(nlogn);具体推算另外开文叙述。
  2. 就空间复杂度来说,主要是递归造成的栈空间的使用,最好情况,递归树的深度为log2n,其空间复杂度也就为O(logn),最坏情况,需要进行n‐1递归调用,其空间复杂度为O(n),平均情况,空间复杂度也为O(logn)。

但一般情况来说,快速排序的性能还是很好的,但是因为跳跃性的数据交换,导致算法实际上不能保证原先的两个相等的元素的顺序,所以快速排序是不稳定的排序算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值