排序简介
排序算法的稳定性:排序前两个相等数的前后位置顺序和排序后它们两个的前后位置顺序相同。
内部排序:将需要处理的数据都加载到内存中进行排序。
- 快、希、选、堆不稳定("快些选一堆")
- 插入、选择、交换、归并、基数...
外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。
| 排序方法 | 平均时间 | 最坏时间 | 空间 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | 稳定 | |||
| 插入排序 | 稳定 | |||
| 选择排序 | 不稳定 | |||
| 希尔排序 | 不稳定 | |||
| 堆排序 | 不稳定 | |||
| 快速排序 | 不稳定 | |||
| 归并排序 | 稳定 | |||
| 基数排序 | 稳定 | |||
| 计数排序 | 稳定 | |||
| 桶排序 | 稳定 |
1. 冒泡排序
冒泡排序:重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。越小的元素会经由交换慢慢“浮”到数列的顶端。
// 冒泡排序
public static void bubbleSort(int[] container) {
// 标识是否发生交换
boolean isExchange = false;
for (int i = 0; i < container.length - 1; i++) {
// 每一趟从0开始,给右边确定一个数
for (int j = 0; j < container.length - 1 - i; j++) {
if (container[j] > container[j + 1]) {
isExchange = true;
int tmp = container[j];
container[j] = container[j + 1];
container[j + 1] = tmp;
}
}
// 如果某趟排序一次交换都没有发生,则说明已经有序了
if (isExchange == false)
return;
else
// 重置isExchange,进行下次判断
isExchange = false;
}
}
2. 插入排序
插入排序:每次从无序区取一个数,当该数比有序区某个数大时,将该数插入到有序区这个数的后面。
// 插入排序
public static void insertionSort(int[] container) {
// 有序区最初只有容器的第0个元素,因为只有一个元素的序列不用排序就是有序的
for (int j = 1; j < container.length; j++) {
// key表示待插入的数
int key = container[j];
// i表示在哪一个元素后插入,初值为有序区右边第一个元素的索引
int i = j - 1;
/*
Insert A[j] into the sorted sequence A[1..j-1].
i >= 0保证数组不越界
当待插入数比有序区数大时进行插入
container[i] > key表示待插入的数还没找到插入的位置
*/
while (i >= 0 && container[i] > key) {
// 将container[i]后移
container[i + 1] = container[i];
i--;
}
// 当退出循环时,说明插入的位置找到了,是i + 1
// 若i + 1 == j,则说明不用移动;这个if判断可有可无
if (i + 1 != j)
container[i + 1] = key;
}
}
3. 选择排序
选择排序:每一趟在无序区选出一个最小的,然后与无序区首元素交换。
// 选择排序
public static void selectionSort(int[] container) {
for (int i = 0; i < container.length; i++) {
int minIndex = i;
for (int j = i + 1; j < container.length; j++) {
if (container[j] < container[minIndex])
minIndex = j;
}
if (minIndex != i) {
int tmp = container[minIndex];
container[minIndex] = container[i];
container[i] = tmp;
}
}
}
4. 希尔排序
希尔排序又称“缩小增量排序”,是插入排序的一种更高效的改进版本。希尔排序是把记录按下标的一定增量分组,对每组使用插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
// 希尔排序
public static void shellSort(int[] container) {
// 进行分组,增量初值为容器容量的一半,每次缩小为原来的一半
for (int increment = container.length / 2; increment > 0; increment /= 2) {
/**对各个分组进行插入排序:
* increment之前的元素为各组初始时的有序区
* i每次增1表示:
* 并不是一次性将一个组排序完,再对另一个组排序;
* 而是轮流对各组进行排序,每次只给各组排一个元素
*/
for (int i = increment; i < container.length; i++) {
// 将container[i]插入到其所在分组的正确位置
insertElement(container, i, increment);
}
}
}
private static void insertElement(int[] container, int index, int increment) {
// key表示待插入的数
int key = container[index];
// i表示在哪一个元素后插入,初值为有序区右边第一个元素的索引
int i = index - increment;
while (i >= 0 && container[i] > key) {
// 将container[i]后移到container[i + increment]
container[i + increment] = container[i];
i -= increment;
}
// 若i + increment == index,则说明不用移动;这个if判断可有可无
if (i + increment != index)
container[i + increment] = key;
}
5. 堆排序
堆是一棵完全二叉树,堆中某个节点的值总是不大于或不小于其父节点的值。
根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
优先队列具有最高级先出的行为特征,分为最大优先队列和最小优先队列,通常采用堆数据结构来实现。
import java.util.Arrays;
/**
* 最大堆与最大优先队列
* 与堆排序相关的方法:maxHeapify、buildMaxHeap、heapSort
* 最大优先队列的方法:maximum、extractMax、increaseKey、insert
*/
public class Heap {
// 有多少个堆元素存储在数组中
private int size;
private int[] container;
public Heap(int[] container) {
this.container = container;
size = container.length;
buildMaxHeap();
}
// i的父节点
private int parent(int i) {
return (i - 1) / 2;
}
// i的左孩子
private int left(int i) {
return 2 * i + 1;
}
// i的右孩子
private int right(int i) {
return 2 * i + 2;
}
// 交换a和b
private void exchange(int[] container, int x, int y) {
int tmp = container[x];
container[x] = container[y];
container[y] = tmp;
}
// 最大堆堆化
private void maxHeapify(int[] container, int i) {
int left = left(i);
int right = right(i);
int largest = i;
if (left < size && container[left] > container[i])
largest = left;
if (right < size && container[right] > container[largest])
largest = right;
if (largest != i) {
exchange(container, i, largest);
maxHeapify(container, largest);
}
}
// 建立最大堆
public void buildMaxHeap() {
size = container.length;
// container[length/2..length-1]为树的叶结点
// 从右到左,从下到上,堆化每一颗子树
for (int i = container.length / 2 - 1; i >= 0; i--)
maxHeapify(container, i);
}
// 堆排序
public void heapSort() {
buildMaxHeap();
for (int i = container.length - 1; i > 0; i--) {
// 将当前堆中的最大元素与第i个交换
exchange(container, 0, i);
// 堆中元素,从右到左,少一个
size--;
// 重新堆化
maxHeapify(container, 0);
}
}
// 获取最大堆的最大元素
public int maximum() {
return container[0];
}
// 去掉并返回container中的最大元素
public int extractMax() {
int max = container[0];
// 将最后一个元素放到根的位置
container[0] = container[size - 1];
// 堆中元素减一
size--;
container = Arrays.copyOf(container, size);
// 重新堆化
maxHeapify(container, 0);
return max;
}
// 将第i个元素的值增加到key
public void increaseKey(int i, int key) {
if (key < container[i])
throw new RuntimeException();
container[i] = key;
// 若增加后的值比其父节点大,则将其与父节点交换
while (i > 0 && container[parent(i)] < container[i]) {
exchange(container, i, parent(i));
i = parent(i);
}
}
// 将key插入到container
public void insert(int key) {
// 堆中元素加一
size++;
container = Arrays.copyOf(container, size);
// 将新增加的空间的值由最小值增加到key
container[size - 1] = Integer.MIN_VALUE;
increaseKey(size - 1, key);
// 以上操作,相当于将key插入到了container
}
@Override
public String toString() {
return "Heap{" +
"size=" + size +
", container=" + Arrays.toString(container) +
'}';
}
public static void main(String[] args) {
int[] A = {16, 4, 10, 14, 7, 9, 3, 2, 8, 1};
Heap heap = new Heap(A);
System.out.println("最大堆:\n" + heap);
heap.heapSort();
System.out.println("进行堆排序后:\n" + heap);
heap.buildMaxHeap();
System.out.println("重新建立的最大堆:\n" + heap);
System.out.println("堆中最大值:\n" + heap.maximum());
int max = heap.extractMax();
System.out.println("去掉最大值" + max + "后的堆:\n" + heap);
heap.increaseKey(1, 20);
System.out.println("将" + 10 + "增加至20后的堆:\n" + heap);
heap.insert(30);
System.out.println("插入30后的堆:\n" + heap);
}
}
6. 快速排序
快速排序使用了分治法,如对一个子数组A[p..r]进行快速排序的步骤:
- 分解:数组A[p..r]被划分为两个(可能为空)子数组A[p..q-1]和A[q+1..r],
使得A[p..q—1]中的每一个元素都小于等于A[q],而A[q]也小于等于A[q+1..r]中的每个元素。其中,计算下标q也是划分过程的一部分。 - 解决:通过递归调用快速排序,对子数组A[p..q—1]和A[q+1..r]进行排序。
- 合并:因为子数组都是原址排序的,所以不需要合并操作:数组A[p..r]已经有序。
// 快速排序,对[p, r]区间内的元素排序
public static void quickSort(int[] A, int p, int r) {
if (p < r) {
int q = partition(A, p, r);
quickSort(A, p, q - 1);
quickSort(A, q + 1, r);
}
}
// 数组的划分,是快速排序的关键部分
private static int partition(int[] A, int p, int r) {
// 围绕A[r]来划分子数组A[p..r]
int x = A[r];
// i是{ <= x区域}的最右边元素的索引
int i = p - 1;
// j是{ > x区域}的后面一个元素的索引
for (int j = p; j < r; j++) {
// { <= x区域}和{ > x区域}相邻
// 如果A[j] <= x,则将A[j]和{ > x区域}最左边的元素交换
if (A[j] <= x) {
i++;
exchange(A, i, j);
}
// 如果A[j] > x,则j++,即将A[j]放入{ > x区域}的最右边
}
exchange(A, i + 1, r);
// 返回划分后x所在的索引
return i + 1;
}
// 交换a和b
private static void exchange(int[] A, int x, int y) {
int tmp = A[x];
A[x] = A[y];
A[y] = tmp;
}
7. 归并排序
归并排序算法完全遵循分治模式。直观上其操作如下:
- 分解:分解待排序的n个元素的序列成各具n/2个元素的两个子序列。
- 解决:使用归并排序递归地排序两个子序列。
- 合并:合并两个已排序的子序列以产生已排序的答案。
归并排序算法的关键操作是“合并”步骤中两个已排序序列的合并。
// 归并排序,对[p, r]区间内的元素排序
public static void mergeSort(int[] A, int p, int r) {
if (p < r) {
// 向下取整
int q = (p + r) / 2;
// 排序左边
mergeSort(A, p, q);
// 排序右边
mergeSort(A, q + 1, r);
// 合并
merge(A, p, q, r);
}
}
// 合并A[p..q]和A[q+1..r]
private static void merge(int[] A, int p, int q, int r) {
// A[p..q]的长度
int leftLength = q - p + 1;
// A[q+1..r]的长度
int rightLength = r - q;
// 多出来的最右边的一个空间充当哨兵
int[] left = new int[leftLength + 1];
int[] right = new int[rightLength + 1];
// 将A[p..q]复制到left[0..leftLength-1]
for (int i = 0; i < leftLength; i++) {
left[i] = A[p + i];
}
// 将A[q+1..r]复制到right[0..rightLength-1]
for (int j = 0; j < rightLength; j++) {
right[j] = A[q + 1 + j];
}
// 哨兵
left[leftLength] = Integer.MAX_VALUE;
right[rightLength] = Integer.MAX_VALUE;
int i = 0;
int j = 0;
// 合并
for (int k = p; k <= r; k++) {
if (left[i] <= right[j]) {
A[k] = left[i];
i++;
} else {
A[k] = right[j];
j++;
}
}
}
8. 基数排序
基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机上的贡献。它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
基数排序的方式可以采用最低位优先(LSD)或最高位优先(MSD),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
基数排序是以空间换时间
// 基数排序,对n个d位的正整数进行排序
public static void radixSort(int[] container) {
// 创建一个拥有10个桶的容器,每个桶是一个线性表
LinkedList<Integer>[] buckets = new LinkedList[10];
for (int i = 0; i < 10; i++) {
buckets[i] = new LinkedList<>();
}
// 获取最大值的位数
int d = (Arrays.stream(container).max().getAsInt() + "").length();
// 采用最低位优先,根据每一位进行排序
for (int i = 0; i < d; i++) {
// 将容器中的元素根据第i位,依次放入对应的桶中
for (int j = 0; j < container.length; j++) {
// digit表示元素第i位的数字
int digit = container[j] / (int) Math.pow(10, i) % 10;
// 将container[j]放入第digit个桶中
buckets[digit].add(container[j]);
}
// 放完后,即已经按照第i位的顺序排好了
// 将桶中元素依次放入原容器,index代表原容器的索引
int index = 0;
for (int j = 0; j < buckets.length; j++) {
for (int k = 0; k < buckets[j].size(); k++) {
// 将第j个桶中的第k个元素赋值给原容器
container[index++] = buckets[j].get(k);
}
// 清空第j个桶
buckets[j].clear();
}
}
}