参考书目:《大话数据结构》
一、冒泡排序(优化版)
0.基本思想:一种交换排序,两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录位置。
1.思路图解:
2.代码实现:
public class BubbleSort {
public static int[] sort(int[] array) {
int i, j;
int len = array.length;
int[] copyArray = Arrays.copyOf(array, len);
boolean flag = true;
for (i = 0; i < len - 1 && flag; i++) {
flag = false;
for (j = len - 2; j >= i; j--) {
if (copyArray[j] > copyArray[j + 1]) {
swap(copyArray, j, j + 1);
flag = true;
}
}
}
return copyArray;
}
private static void swap(int[] array, int x, int y) {
int tmp = array[x];
array[x] = array[y];
array[y] = tmp;
}
}
3.分析:
- 平均时间:O (n^2)
- 最差时间:O (n^2)
- 稳定度:稳定
- 额外空间:O (1)
- 最佳实践:n 小时好
二、简单选择排序
0.基本思想:通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1<=i<=n)个记录交换。
1.思路图解:
2.代码实现:
public class SelectSort {
public static int[] sort(int[] array) {
int i, j;
int len = array.length;
int[] copyArray = Arrays.copyOf(array, len);
int minIndex = 0;
for (i = 0; i < len - 1; i++) {
minIndex = i;
for (j = i + 1; j < len; j++) {
if (copyArray[j] < copyArray[minIndex]) {
minIndex = j;
}
}
if (minIndex != i) {
swap(copyArray, i, minIndex);
}
}
return copyArray;
}
private static void swap(int[] array, int x, int y) {
int tmp = array[x];
array[x] = array[y];
array[y] = tmp;
}
}
3.分析:
- 平均时间:O (n^2)
- 最差时间:O (n^2)
- 稳定度:不稳定
- 额外空间:O (1)
- 最佳实践:n 小时好
- 最大特点就是交换移动数据次数相当少,性能上略优于冒泡排序
三、直接插入排序
0.基本思想:将一个记录插入到以及排好序的有序表中,从而得到一个新的、记录数增1的有序表。
1.思路图解:
2.代码实现:
public class InsertSort {
public static int[] sort(int[] array) {
int i, j;
int len = array.length;
int[] copyArray = Arrays.copyOf(array, len);
int tmp = 0;
for (i = 0; i < len - 1; i++) {
if (copyArray[i] > copyArray[i + 1]) {
tmp = copyArray[i + 1];
for (j = i; j >= 0 && copyArray[j] > tmp; j--) {
copyArray[j + 1] = copyArray[j];
}
copyArray[j + 1] = tmp;
}
}
return copyArray;
}
}
3.分析:
- 平均时间:O (n^2)
- 最差时间:O (n^2)
- 稳定度:稳定
- 额外空间:O (1)
- 最佳实践:序列中基本有序时较好
- 优于冒泡和简单选择排序
四、希尔排序
0.基本思想:把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,所有记录恰被分成一组,算法便终止。因此,将关键字较小的记录,不是一步一步地往前挪动,而是跳跃式地往前移,从而使得每完成一轮循环后,整个序列就朝着有序坚实地迈进一步。
1.思路图解:
2.代码实现:
public class ShellSort {
public static int[] sort(int[] array) {
int i, j;
int len = array.length;
int[] copyArray = Arrays.copyOf(array, len);
int increment = len - 1;
int tmp = 0;
//用 do-while 是为了 len = 2 时这种特殊情况
do {
increment = increment / 3 + 1;
for (i = 0; i < len - increment; i++) {
if (copyArray[i] > copyArray[i + increment]) {
tmp = copyArray[i + increment];
for (j = i; j >= 0 && copyArray[j] > tmp; j -= increment) {
copyArray[j + increment] = copyArray[j];
}
copyArray[j + increment] = tmp;
}
}
} while (increment > 1);
return copyArray;
}
}
3.分析:
- 平均时间:O (nlogn)
- 最差时间:O (n^s, 1 < s < 2)
- 稳定度:不稳定
- 额外空间:O (1)
- 由于记录是跳跃式移动,所以这并不是一种稳定的排序算法
五、堆排序
0.基本思想:将待排序的序列构造成一个大顶锥(适用于从小到大排序),此时整个序列的最大值就是堆顶的根节点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余n-1个序列重新构造成一个堆,这样就会得到n个元素的次大值。如此反复执行,便能得到一个有序序列。
1.思路图解:
2.代码实现:
public class HeapSort {
public static int[] sort(int[] array) {
int i;
int n = array.length;
//构建大顶堆
for (i = (n - 1) / 2; i >= 0; i--) {
heapAdjust(array, i, n - 1);
}
//对大顶堆进行堆排序
for (i = n - 1; i > 0; i--) {
swap(array, 0, i);
heapAdjust(array, 0, i - 1);
}
return array;
}
private static void heapAdjust(int[] array, int start, int end) {
int j;
int tmp = array[start];
for (j = 2 * start; j <= end; j *= 2) {
if (j < end && array[j] < array[j + 1]) {
j++; //j为当前start结点,和此结点的左右孩子直接的较大值的下标
}
if (tmp >= array[j]) {
break; //不需要调整,这时a[start]本来就是三角形的较大值
}
array[start] = array[j];
start = j;
}
array[start] = tmp; //若经历了for构造,则应为最初的array[start]找一个归宿
}
private static void swap(int[] array, int x, int y) {
int tmp = array[x];
array[x] = array[y];
array[y] = tmp;
}
}
3.分析:
- 平均时间:O (nlogn)
- 最差时间:O (nlogn)
- 稳定度:不稳定
- 额外空间:O (1)
- 最佳实践:n 大时好
- 由于记录的比较与交换是跳跃式进行,因此也是一种不稳定的排序算法
- 由于初始构建堆所需比较次数较多,因此不适合待排序序列个数较少的情况
六、归并排序(递归版)+归并排序(递归优化版)
0.基本思想:初始序列含有n个元素,则可看成是n个有序子序列,每个子序列的长度为1,然后两两归并,得到Math.ceil(n/2)个长度为2或1的有序子序列;再两两归并,…,如此重复,直到得到一个长度为n的有序序列位置,这种排序方法称为2路归并排序。
1.思路图解:
2.代码实现:
//递归实现
public class MergeSort {
public static int[] sort(int[] a, int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
sort(a, low, mid);
sort(a, mid + 1, high);
//左右归并
merge(a, low, mid, high);
}
return a;
}
public static void merge(int[] a, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;
int j = mid + 1;
int k = 0;
// 把较小的数先移到新数组中
while (i <= mid && j <= high) {
if (a[i] < a[j]) {
temp[k++] = a[i++];
} else {
temp[k++] = a[j++];
}
}
// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = a[i++];
}
// 把右边边剩余的数移入数组
while (j <= high) {
temp[k++] = a[j++];
}
// 把新数组中的数覆盖nums数组
for (int index = 0; index < temp.length; index++) {
a[low + index] = temp[index];
}
}
}
2.5.代码实现(优化版):
public class MergeSort2 {
public static void sort(int[] a) {
int n = a.length - 1;
MSort(a);
}
private static void MSort(int[] SR) {
int n = SR.length - 1;
int[] TR = new int[n + 1];
int k = 1;
while (k < n) {
MergePass(SR, TR, k, n);
k = 2 * k;
MergePass(TR, SR, k, n);
k = 2 * k;
}
}
// 将SR[s..n]归并为有序的TR[s..n]
private static void MergePass(int[] SR, int[] TR, int s, int n) {
int i = 1;
int j;
while (i <= n - 2 * s + 1) {
Merge(SR, TR, i, i + s - 1, i + 2 * s - 1);
i = i + 2 * s;
}
if (i < n - s + 1) {
Merge(SR, TR, i, i + s - 1, n);
} else {
for (j = i; j <= n; j++) {
TR[j] = SR[j];
}
}
}
// 将有序的SR[s..m]和SR[m+1..n]归并为有序的TR[i..n]
private static void Merge(int[] SR, int[] TR, int s, int m, int n) {
int j, k, l; // k为左块的起始下标,j为右块的起始下标
for (k = s, j = m + 1; s <= m && j <= n; k++) { // SR中记录由小到大归并入TR
if (SR[s] < SR[j]) {
TR[k] = SR[s++];
} else {
TR[k] = SR[j++];
}
}
if (s <= m) {
for (l = 0; l <= m - s; l++) {
TR[k + l] = SR[s + l]; // 将剩余的SR[s..m]复制到TR
}
}
if (j <= n) {
for (l = 0; l <= n - j; l++) {
TR[k + l] = SR[j + l]; // 将剩余的SR[j..m]复制到TR
}
}
}
}
3.分析:
-
平均时间:O (nlogn)
-
最差时间:O (nlogn)
-
稳定度:稳定
-
最佳实践:n 大时好
-
空间复杂度:O(nlogn) 和 O(n)
-
需要两两比较,不存在跳跃,是一种稳定的排序算法
-
比较占用内存,但效率高
-
尽量用非递归版(优化版)
七、快速排序+快速排序(四度优化版)
0.基本思想:通过一趟排序将待排序记录分割成独立的两部分,其中每一部分记录的关键字均比另一部分记录的关键字小,则可以分别对着两部分记录继续进行排序,以达到整个序列有序的目的。
1.思路图解:
2.代码实现:
public class QuickSort {
public static void sort(int[] a) {
int n = a.length - 1;
QSort(a, 1, n);
}
private static void QSort(int[] a, int low, int high) {
if (low < high) {
int pivot = Partition(a, low, high); // 将a[low..high]一分为二,算出枢轴下标pivot
QSort(a, low, pivot - 1); // 对低子表递归排序
QSort(a, pivot + 1, high); // 对高子表递归排序
}
}
// 交换顺序表a中子表的记录,使枢轴记录到为,并返回其位置
private static int Partition(int[] a, int low, int high) {
int pivotkey = a[low]; // 用子表的第一个记录作枢轴记录
while (low < high) { // 从表的两端交替向中间扫描
while (low < high && a[high] >= pivotkey) {
high--;
}
swap(a, low, high); // 将比枢轴值小的记录交换到低端
while (low < high && a[low] <= pivotkey) {
low++;
}
swap(a, low, high); // 将比枢轴值大的记录交换到高端
}
return low; // 最终low == high,所有返回枢轴所在位置
}
private static void swap(int[] a, int x, int y) {
int temp;
temp = a[x];
a[x] = a[y];
a[y] = temp;
}
}
2.代码实现(优化版):
public class QuickSortUp {
// 1.优化选取枢轴(三数取中),选取更较为合适的枢轴值,使得左小右大更均匀;
// 2.优化交换操作为替换,分析得知,swap交换操作本身不必要;
// 3.优化小数组时的排序方案为直接插入排序,而对于大数组则采用快排;
// 4.优化递归操作,将对高子表的递归转为只对低子表的迭代版递归
public static void sort(int[] a) {
int n = a.length - 1;
QSort(a, 1, n);
}
private static void QSort(int[] a, int low, int high) {
int ORDINARY_LEN = 7; // 数组长度阈值,当小于时,可用直接插入排序
int pivot; // 枢轴的下标,将某个数放在此位置,使得它左边的值都比它小,右边的都比它大
if ((high - low) > ORDINARY_LEN) {
while (low < high) {
pivot = Partition(a, low, high); // 将a[low..high]一分为二,算出枢轴下标pivot
QSort(a, low, pivot - 1); // 对低子表递归排序
low = pivot + 1; // 尾递归
}
} else {
InterSort.sort(a);
}
}
// 交换顺序表a中子表的记录,使枢轴记录到为,并返回其位置
private static int Partition(int[] a, int low, int high) {
int pivotkey;
int m = low + (high - low) / 2;
if (a[low] > a[high]) {
swap(a, low, high);
}
if (a[m] > a[high]) {
swap(a, high, m);
}
if (a[m] > a[low]) {
swap(a, m, low);
}
pivotkey = a[low]; // 此时a[low]为三数取中得到的中间值
a[0] = pivotkey; // 哨兵
while (low < high) { // 从表的两端交替向中间扫描
while (low < high && a[high] >= pivotkey) {
high--;
}
a[low] = a[high]; //改交换为替换
while (low < high && a[low] <= pivotkey) {
low++;
}
a[high] = a[low];
}
a[low] = a[0];
return low; // 最终low == high,所有返回枢轴所在位置
}
private static void swap(int[] a, int x, int y) {
int temp;
temp = a[x];
a[x] = a[y];
a[y] = temp;
}
}
3.分析:
- 平均时间:O (nlogn)
- 最差时间:O (n^2)
- 稳定度:不稳定
- 最佳实践:n 大时好
- 空间复杂度:O(logn) 和 O(n)
- 由于关键字的比较和交换是跳跃进行的,因此也不稳定
- 优化后性能提升