常见排序算法

Posted at:
February 8, 2018

1 简介

本文参考 白话经典算法之七大排序 ,介绍常见的排序算法,包括原理和实现。

2 冒泡排序

冒泡排序主要原理就是依次通过交换排序相邻元素,使得较大元素移到后面,这样第N轮最差能够排序好前N大元素。
当然对于N个元素来说,最多只需要N-1轮。

冒泡排序第一种优化是每一轮检查是否发生交换,如果没有则说明排序已经完成。

冒泡排序第二种优化是每一轮记录最后一次交换的位置,下一轮只要遍历到这个位置即可。

3 归并排序

归并排序的重点在于合并。
合并两个有序的序列是比较容易的:
比较两序列第一个元素,取出较小的,然后重复上述步骤直到有序列为空,然后从另一序列取出剩余元素。
容易知道单个元素序列是有序的;两个元素的序列可以拆分为两个单元素序列后合并成新的有序序列。
既然如此,那么对于任意长度序列,我们总是可以将其二分为两个相邻序列,然后通过递归完成合并工作。
这个递归过程最终会把序列拆分为单元素序列,而单元素序列是有序的,递归收敛,开始合并结果。
这样通过先递归的分解数列,再合并数列就完成了归并排序。
代码如下:

4 快速排序

快速排序基本思路如下:

  1. 先从数列中选出一个数(一般就是第一个数)作为基准数
  2. 比基准数大的数移到它的右边,小于或等于它的数移到它的左边
  3. 对于基准数左右两边区间重复第2步,直到区间内只有一个数

其中第二步可以简单理解为挖坑填数。
挖坑填数步骤如下:

  1. frnt = l; back = r; 将基准数挖出形成第一个坑arr[frnt]
  2. back–由后向前找比基准数小的数,填坑arr[frnt],形成新坑arr[back]
  3. frnt++由前向后找比基准数大的数,填坑arr[back],形成新坑arr[frnt]
  4. 再重复执行第2,3 二步直到 frnt==back,将基准数填坑arr[frnt]

以一个数组作为示例,取区间第一个数为基准数。

0 1 2 3 4 5 6 7 8 9
72 6 57 88 60 42 83 73 48 85

frnt=0, back=8

0 1 2 3 4 5 6 7 8 9
48 6 57 88 60 42 83 73 48 85

frnt=3, back=8

0 1 2 3 4 5 6 7 8 9
48 6 57 88 60 42 83 73 88 85

frnt=3, back=5

0 1 2 3 4 5 6 7 8 9
48 6 57 42 60 42 83 73 88 85

frnt=5, back=5

0 1 2 3 4 5 6 7 8 9
48 6 57 42 60 72 83 73 88 85

从上面可以看到,比72小的数都在72左边,比72大的数都在72右边。

代码如下:

5 大小堆与堆排序

5.1 二叉堆的特性

  • 是完全二叉树或者近似完全二叉树
  • 父节点的键值总是 >=(或者 <=)子节点的键值
  • 每个节点的左右子树都是二叉堆

当父节点键值总是>=子节点键值时称为最大堆,当父节点键值总是<=子节点键值时称为最小堆。
大小堆就是指最大堆和最小堆,下文主要讲述最大堆。

5.2 堆的存储

由于大小堆是(近似)完全二叉树,因此可以用数组来表示。

对于索引为idx的节点,其父节点索引为(idx-1)/2。
对于索引为idx的节点,其左节点索引为2*idx+1,其右节点索引为2*idx+2。

5.3 堆的元素插入

插入元素到堆都是将其放在数组最后,可以发现从新元素的父结点到根节点是有序序列,只要将其插入到这个有序序列即可。

5.4 堆的元素删除

堆只能删除堆顶元素,实际操作是把最后的元素与其交换,然后从堆顶向下调整、恢复堆。
向下调整的过程,主要是确保父节点>=子节点(最大堆),如果不满足则跟子节点较大的进行交换。
上述过程在发生交换的情况下需要递归向下进行。

5.5 堆的建立

我们知道叶子节点是二叉堆,所以堆的建立只要从最后的非叶子节点开始往前调整堆即可。

5.6 堆排序

最大堆建立好之后,堆顶(数组第一个元素)就是堆中最大的元素。
这样的话,只要不断重复获取、移除堆顶直到堆变空为止,我们就得到了排序好的序列。
实际上我们并不需要移除堆顶,只需要与最后的元素交换,然后假装堆中元素减一执行恢复堆操作即可。
这样的话,可以减少额外空间的使用、节省拷贝排序后数据的操作。

实现从小到大排列的堆排序代码如下:

6 插入排序

插入排序的基本思路是:

  • 单个元素的序列是有序序列
  • 依次将后续元素插入到前面的有序序列

怎么将元素插入到前面的有序序列?
一种方法是通过交换,前面的元素较大则交换,一直往前交换到不大于的元素为止。

另外一种方法就是通过后移腾出空位,效率更高,不过编码容易出错。

7 选择排序

选择排序的思路是:

  1. 初始时,序列为无序区间[0, size),idx==0
  2. 在无序区间[idx, size)中选取最小的元素,将其与arr[idx]交换形成有序区间[0, idx]
  3. 重复第2步直到无序区间为空

8 希尔(Shell)排序

希尔排序是分组+插入排序。

版本一:

从gap开始,每个元素与自己分组内的元素进行插入排序。
版本二:

9 基数排序/RadixSort/BucketSort

基数排序(LSD最低位优先)是依次按位数(个,十,百,千,万…)将元素放入0~9号通中排成新序列,最终完成排序。
首先我们按照正常思维实现:

通过 partial_sum 我们可以在静态数组上实现,而不用动态数组。

Modified at: February 8, 2018