排序
稳定排序: 如果a=b并且a出现在b的前面,排序之后a仍然在b的前面。如冒泡排序、插入排序、归并排序、计数排序、桶排序、基数排序。
不稳定排序:如果a=b并且a出现在b的前面,排序之后b可能出现在a的前面。如选择排序、希尔排序、快速排序、堆排序。
1 插入排序
最简单的排序算法之一,由N-1趟排序组成。对于未排序数据,在已排序序列中从后向前扫描,把已排序的元素向后移,找到相应的位置并插入数据。
空间复杂度: O(1)
时间复杂度: 平均O(N2),最好O(N),最坏O(N2)。
public static > void insertionSort(AnyType[] a)
{
int j;
for (int p = 1; p < a.length; p++) { //第p次排序,将第p个数插入已排序好的前p-1个数中
AnyType tmp = a[p];
for (j = p; j > 0 && tmp.compareTo(a[j-1]) < 0; j--) {
a[j] = a[j - 1];
}
a[j] = tmp;
}
}
2 希尔排序
希尔排序是改进后的简单插入排序,也称为缩小增量排序。
希尔排序使用一个增量序列,运行时间也依赖于增量序列的选择,希尔建议的序列为{N/2, (N/2)/2, ...,1},即希尔增量。按照增量的个数,进行k趟排序,每趟排序,根据对应的增量,将待排序列分割成若干长度为m的子序列,各个子序列分别进行插入排序。
原始数组 | 8 | 9 | 1 | 7 | 2 | 3 | 5 | 4 | 6 | 0 |
---|---|---|---|---|---|---|---|---|---|---|
增量5排序后 | 3 | 5 | 1 | 6 | 0 | 8 | 9 | 4 | 7 | 2 |
增量2排序后 | 0 | 2 | 1 | 4 | 3 | 5 | 7 | 6 | 9 | 8 |
增量1排序后 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
空间复杂度: O(1)
时间复杂度:希尔增量下最坏O(N2),Hibbard增量下最坏O(N3/2)
希尔排序开始时,元素无序度高,但是每组进行插入排序的元素少,速度快;后期元素基本有序,插入排序对于有序的序列效率高,所以希尔排序时间复杂度低于简单插入排序。
public static > void shellSort(AnyType[] a) {
int j;
for (int gap = a.length / 2; gap > 0; gap /= 2)
{
for (int i = gap; i < a.length; i++)
{
AnyType tmp = a[i];
for (j = i; j >= gap && tmp.compareTo(a[j - gap]) < 0; j-= gap)
a[j] = a[j - gap];
a[j] = tmp;
}
}
}
3 选择排序
时间复杂度稳定为O(N^2)的简单直观排序方法。首先在未排序的序列中选出最小的元素放在排序序列的起始位置,然后在未排序的序列中继续寻找最小的元素,将其添加到已排序的序列的末尾,直到所有元素排序完毕。
空间复杂度:O(1)
时间复杂度:O(N2)
public static > void selectionSort(AnyType[] a) {
for (int i = 0; i < a.length; i++) {
int minIndex = i;
for (int j = i; j < a.length; j++) {
if (a[j].compareTo(a[minIndex]) < 0) {
minIndex = j;
}
}
AnyType temp = a[minIndex];
a[minIndex] = a[i];
a[i] = temp;
}
}
4 堆排序
堆排序是选择排序的一种,堆分为最大堆(父结点值总是大于子结点)和最小堆,是完全二叉树。
父节点索引i,左儿子索引2i+1,右儿子索引2i+2。
将初始待排序序列构建成最大堆,将堆顶元素与最后一个元素交换,产生无序部分和有序部分,将无序部分重新调整为最大堆,并将堆顶元素与无序区最后一个元素交换。如此重复构建与交换,直到有序部分元素为n-1,则完成排序。
空间复杂度:O(1)
时间复杂度:O(NlogN)
public static void heapify(int[] tree, int n, int i) { //根结点不行,子树满足堆结构
//i为当前需要HEAPIFY的根节点
int c1 = 2 * i + 1;
int c2 = 2 * i + 2;
int max = i;
if (c1 < n && tree[c1] > tree[max]) {
max = c1;
}
if (c2 < n && tree[c2] > tree[max]) {
max = c2;
}
if (max != i) //根结点小于子结点时
{
swap(tree, max, i);
heapify(tree, n, max);
}
}
public static void buildHeap(int[] tree, int n) { //完全乱序时
int lastNode = n - 1;
int parent = (lastNode - 1) / 2;
int i;
for (i = parent; i >= 0; i--) {
heapify(tree, n, i);
}
}
public static void swap(int[] tree, int i, int j) {
int temp = tree[i];
tree[i] = tree[j];
tree[j] = temp;
}
public static void heapSort(int[] tree, int n) {
buildHeap(tree, n);
int i;
for (i = n - 1; i >= 0; i--) {
swap(tree, i, 0);
heapify(tree, i, 0);
}
}
5 冒泡排序
重复比较相邻的元素,如果第一个数比第二个大,则交换位置,最开始从第一对一直交换到最后一对,一趟排序后则最大数存在在最后位置。对于未排序的部分重复上述交换操作。
空间复杂度:O(1)
时间复杂度:平均O(N2),最好O(N),最坏O(N2)
代码中引入boolean didSort进行判断,才能使得最优情况下时间复杂度为O(N)
public static void bubbleSort(int[] a) {
boolean didSort;
for (int i = a.length - 1; i >= 0; i--)
{
didSort = false;
for (int j = 0; j < i; j++)
{
if (a[j] > a[j + 1])
{
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
didSort = true;
}
}
if (didSort == false) //已经排序好,直接退出,可使最优情况下时间复杂度为O(n)
return;
}
}
6 归并排序
归并排序是分治算法的一个典型应用,性能不受输入数据的影响,时间复杂度始终是O(NlogN),代价是需要额外O(N)的内存空间,但是时间受比较元素在额外空间数组中移动等速度影响。通过先对子序列排序,再将两个子序列合并到一个新的序列中。
把长度为n的输入序列分成两个长度为n/2的子序列,对这两个子序列再分别采用归并排序,将排序好的两个子序列合并成一个最终的排序序列。
时间复杂度:O(NlogN)
空间复杂度:O(N)
public class MergeSort {
public static int[] mergeSort(int[] array) {
if (array.length < 2)
return array;
int mid = array.length / 2;
int[] left = Arrays.copyOfRange(array, 0, mid);
int[] right = Arrays.copyOfRange(array, mid, array.length);
return merge(mergeSort(left), mergeSort(right));
}
private static int[] merge(int[] left, int[] right) {
int[] result = new int[left.length + right.length];
for (int index = 0, l = 0, r = 0; index < result.length; index++) {
if (l >= left.length) //左子序列已经全部合并入新序列中
result[index] = right[r++];
else if (r >= right.length)
result[index] = left[l++];
else if (left[l] > right[r])
result[index] = right[r++];
else
result[index] = left[l++];
}
return result;
}
7 快速排序
首先从序列中随机选择一个基准元素,逐个比较各元素与基准元素大小,将小的元素放在基准前面,大的元素放在后面,这样一轮下来序列就被分为两个部分,左半部分比基准小,右半部分比基准大。接着再分别对这两部分进行相同的操作。
时间复杂度:平均O(NlogN),最好O(NlogN),最坏O(N2)
空间复杂度:O(logN)
public static void quickSort(int[] arr, int start, int end) {
if (start >= end)
return;
//把需要排序的部分第一个元素作为基准
int stard = arr[start];
//记录需要排序的下标
int low = start;
int high = end;
//循环找比基准小和比基准大的数
while (low < high) {
//右边数字比基准大
while (low < high && arr[high] >= stard)
{
high--;
}
//使用右边的数字替换左边的数
arr[low] = arr[high];
//左边数字比基准小
while (low < high && arr[low] <= stard)
{
low++;
}
//使用左边的数字替换右边的数
arr[high] = arr[low];
}
//把基准数字重新赋给所在位置
arr[low] = stard;
quickSort(arr, start, low);
quickSort(arr, low + 1, end);
}