八种常用排序算法总结

交换排序(冒泡排序,快速排序),插入排序(直接插入排序,希尔排序),选择排序(简单选择排序,堆排序),归并排序,基数排序

冒泡排序(Bubble Sort)

冒泡排序步骤:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BubbleSort {
// 冒泡排序
public static void bubbleSort(int[] arr) {
// 控制共比较多少轮
for (int i = 0; i < arr.length - 1; i++) {
// 控制比较的次数
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}

快速排序(Quick Sort)

  快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为较小和较大的2个子序列,然后递归地排序两个子序列。

快速排序步骤

  1. 挑选基准值:从数列中挑出一个元素,称为“基准”(pivot)
  2. 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成,
  3. 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序

  递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class QuickSort {
//快速排序
public static void quickSort(int[] arr, int start, int end) {
if (start < end) {
// 把第0个数作为基准值
int pivot = arr[start];
// 记录需要排序的下标
int low = start;
int high = end;
// 循环找比基准值大的数和比基准值小的数
while (low < high) {
// 右边的数字比基准值大
while (low < high && pivot <= arr[high]) {
high--;
}
// 使用右边的数字替换左边的数
arr[low] = arr[high];
// 如果左边的数字比基准值小
while (low < high && arr[low] <= pivot) {
low++;
}
// 使用左边的数字替换右边的数
arr[high] = arr[low];
}
// 把基准值赋给低(或高)的那一边(这里low==high,所以都一样)
arr[low] = pivot;
// 处理所有小的数字
quickSort(arr, start, low);
// 处理所有大的数字
quickSort(arr, low + 1, end);
}
}
}

插入排序(Insert Sort)

  插入排序原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。举个例子来说:插入排序的工作方式像许多人排序一手扑克牌。开始时,我们的左手为空并且桌面上的牌面向下。然后,我们每次从桌子上拿走一张牌并将它插入左手中正确的位置。为了找到一张牌的正确位置,我们从右到左将它与已在手中的每张牌进行比较。

  插入排序所需的时间取决于输入中元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比对随机顺序的数组或逆序数组进行排序要快得多。

插入排序步骤:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class InsertSort {
//插入排序
public static void insertSort(int[] arr) {
// 遍历所有数字
for (int i = 1; i < arr.length; i++) {
// 把当前遍历数字存起来
int temp = arr[i];
int j = i - 1;
// 如果当前数字比temp大,就一直往前直到找到合适位置
while (j >= 0 && arr[j] > temp) {
// 把前一个数字赋给后一个数字
arr[j + 1] = arr[j];
j--;
}
// 把临时变量赋给不满足条件的后一个值
arr[j + 1] = temp;
}
}
}

希尔排序(Shell Sort)

  希尔排序是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。

  希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

  希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ShellSort {
//希尔排序
public static void shellSort(int[] arr) {
// 遍历所有的步长
for (int d = arr.length / 2; d > 0; d /= 2) {
// 遍历所有的元素
//下面这个for循环取d=1和插入排序一样
for (int i = d; i < arr.length; i++) {
int temp = arr[i];
int j = i - d;
// 遍历本组中所有的元素
while (j >= 0 && arr[j] > temp) {
// 如果当前元素大于加上步长后的那个元素
arr[j + d] = arr[j];
j -= d;
}
arr[j + d] = temp;
}
}
}
}

选择排序(Selection Sort)

  选择排序是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

它有两个很鲜明的特点:
  运行时间与输入无关。为了找出最小的元素而扫描一遍数组并不能为下一遍扫描提供什么信息。这种性质在某些情况下是缺点,因为使用选择排序的人可能会惊讶的发现,一个已经有序的数组或是主键全部相等的数组和一个元素随机排列的数组所用的排序时间竟然一样长!相比之下,其它算法会更善于利用输入的初始状态。
  数据移动是最少的。每次交换都会改变两个数组元素的值,因此选择排序用了\(N\)次交换——交换次数和数组大小是线性关系。其它的任何算法都不具备这个特征(大部分的增长数量级都是线性对数或平方级别)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SelectionSort {
// 选择排序
public static void selectionSort(int[] arr) {
// 遍历所有的数
for (int i = 0; i < arr.length; i++) {
// 这里是记录最小数下标,也可以记录最小的数
int minIndex = i;
// 把当前遍历的数和后面所有的数依次进行比较,并记录下最小的数的下标
for (int j = i + 1; j < arr.length; j++) {
// 如果后面比较的数比记录最小的数小
if (arr[minIndex] > arr[j]) {
// 记录下最小的那个数的下标
minIndex = j;
}
}
// 如果最小的数和当前遍历的数的下标不一致,说明下标为minIndex的数比当前遍历的数更小
if (i != minIndex) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
}

堆排序(Heap Sort)

  堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

通常堆是通过一维数组来实现的。在数组起始位置为0的情形中:

  • 父节点i的左子节点在位置\((2i+1)\)
  • 父节点i的右子节点在位置\((2i+2)\)
  • 子节点i的父节点在位置\(((i-1)/2)\)

  堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余\(n-1\)个元素重新构造成一个堆,这样会得到\(n\)个元素的次小值。如此反复执行,便能得到一个有序序列了

堆排序的步骤

  1. 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
  2. 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class HeapSort {
//堆排序
public static void heapsort(int[] arr) {
// 开始位置是最后一个非叶子节点,即最后一个节点的父节点
int start = (arr.length - 1) / 2;
// 调整大顶堆
for (int i = start; i >= 0; i--) {
maxHeap(arr, arr.length, i);
}
// 先把数组中第0个和堆中最后一个数交换位置,在把前面的处理为大顶堆
for (int i = arr.length - 1; i > 0; i--) {
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
maxHeap(arr, i, 0);
}
}
//调整为大顶堆
public static void maxHeap(int[] arr, int size, int index) {
// 左子节点
int leftNode = 2 * index + 1;
// 右子节点
int rightNode = 2 * index + 2;
int max = index;
// 和两个子节点分别对比,找出最大的节点
if (leftNode < size && arr[leftNode] > arr[max]) {
max = leftNode;
}
if (rightNode < size && arr[rightNode] > arr[max]) {
max = rightNode;
}
// 交换位置
if (max != index) {
int temp = arr[index];
arr[index] = arr[max];
arr[max] = temp;
// 交换位置后可能破坏之前排好的堆,需要重新调整
maxHeap(arr, size, max);
}
}
}

归并排序(Merge Sort)

  归并排序是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。

  归并排序的基本思路是将数组分成二组\(A,B\),如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。要让\(A,B\)两组内部数据有序,可以再将\(A,B\)组各自分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数组,再合并数组就完成了归并排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class MergeSort {
//归并排序
public static void mergesort(int[] arr, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
// 处理左边
mergesort(arr, low, mid);
// 处理右边
mergesort(arr, mid + 1, high);
}
// 用于存储归并后的临时数组
int[] temp = new int[high - low + 1];
// 记录第一个数组中需要遍历的下标
int i = low;
// 记录第二个数组中需要遍历的下标
int j = mid + 1;
// 用于记录在临时数组中存放的下标
int index = 0;
// 遍历两个数组,取出小的数字,放入临时数组中
while (i <= mid && j <= high) {
// 如果第一个数组的数据更小
if (arr[i] <= arr[j]) {
// 把小的数据放入临时数组中
temp[index] = arr[i];
// 让下标向后移一位
i++;
} else {
temp[index] = arr[j];
j++;
}
index++;
}
// 处理多余的数据
while (j <= high) {
temp[index] = arr[j];
j++;
index++;
}
while (i <= mid) {
temp[index] = arr[i];
i++;
index++;
}
// 把临时数组中数据重新存入原数组
for (int k = 0; k < temp.length; k++) {
arr[k + low] = temp[k];
}
}
}

基数排序(Radix Sort)

  基数排序是一种非比较型整数排序算法,其原理是将所有待比较数值(正整数)统一为同样的数字长度,数字较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class RadixSort {
//基数排序
public static void radixsort(int[] arr) {
// 存数组中最大的数字
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
// 求最大数字的位数
int maxlength = (max + "").length();
// 用于临时存储数据的数组
int[][] temp = new int[10][arr.length];
// 用于记录在temp中相应数组存放的数字数量;
int[] counts = new int[10];
// 根据最大长度的数决定比较的次数
for (int i = 0, n = 1; i < maxlength; i++, n *= 10) {
// 把每一个数字分别计算余数
for (int j = 0; j < arr.length; j++) {
// 计算余数
int ys = arr[j] / n % 10;
// 把当前遍历的数据放入指定的数组中
temp[ys][counts[ys]] = arr[j];
// 记录数量
counts[ys]++;
}
// 记录取的元素需要放的位置
int index = 0;
// 把数字取出来
for (int k = 0; k < counts.length; k++) {
// 记录数量数组中当前余数记录的数量不为0
if (counts[k] != 0) {
// 循环取出元素
for (int l = 0; l < counts[k]; l++) {
// 取出元素
arr[index] = temp[k][l];
// 记录下一个位置
index++;
}
// 把数量置为0
counts[k] = 0;
}
}
}
}
}