单链表
- 十大经典排序算法
- 冒泡排序
- 选择排序
- 插入排序
- 归并排序
- 快速排序
- 堆排序
- 计数排序
- 桶排序
1. 十大经典排序算法
十大经典排序算法对比如下:
稳定性的定义
排序后 2 个相等键值的顺序和排序之前它们的顺序相同。
例如:在原序列中,A1=A2
,且A1
在A2
之前,在排序后的序列中,A1
仍在A2
之前,则称这种排序算法是稳定的;否则称为不稳定的。稳定性的意义
若只是简单的进行数字排序,那么稳定性无意义。
排序的内容是一个复杂对象的多个数字属性,且原本的初始顺序存在意义,稳定性的算法可以在二次排序的基础上保持原有排序的意义。
例如:一组原本按价格高低排序的对象,现需要按销量高低排序,使用稳定性算法,可以使相同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。
2. 冒泡排序
/**
* 冒泡排序 平均时间复杂度O(n²),最坏时间复杂度O(n²),最好时间复杂度O(n),空间O(1),稳定
* 1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
* 2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
* 3. 针对所有的元素重复以上的步骤,除了最后一个。
* 4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
*/
private void bubbleSort(int[] array) {
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array.length - i - 1; j++) {
int temp;
if (array[j] > array[j + 1]) {
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
可加个判断优化上面的算法:
private void bubbleSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
boolean sorted = true;
for (int j = 0; j < array.length - i - 1; j++) {
int temp;
if (array[j] > array[j + 1]) {
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
sorted = false;
}
}
if (sorted) break;
}
}
保存循环的最后一次交换下标,进一步优化:
private void bubbleSort(int[] array) {
int lastExchangeIndex = 0;
int sortBorder = array.length - 1;
for (int i = 0; i < array.length - 1; i++) {
boolean sorted = true;
for (int j = 0; j < sortBorder; j++) {
int temp;
if (array[j] > array[j + 1]) {
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
sorted = false;
lastExchangeIndex = j;
}
}
sortBorder = lastExchangeIndex;
if (sorted) break;
}
}
或如下方式实现:
void bubbleSort(int[] arr) {
for(int end = arr.length - 1; end > 0; end--) {
for(int j = 0; j < end; j++) {
if(arr[j] > arr[j+1]) swap(arr, j, j+1);
}
}
}
3. 选择排序
/**
* 简单选择排序 平均时间复杂度O(n²),最坏时间复杂度O(n²),最好时间复杂度O(n²),空间O(1),不稳定
* 1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
* 2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
* 3. 重复第二步,直到所有元素均排序完毕。
*/
private void selectSort(int[] array) {
int len = array.length;
int minIndex, temp;
for (int i = 0; i < len - 1; i++) {
minIndex = i;
for (int j = i + 1; j < len; j++) {
if (array[j] < array[minIndex]) { // 寻找最小的数
minIndex = j; // 将最小数的索引保存
}
}
temp = array[i];
array[i] = array[minIndex];
array[minIndex] = temp;
}
}
或如下方式实现:
// 每次排序时把最小的放在前面
void selectsort(int[] arr) {
for(int i = 0; i < arr.length; i++) {
// 最小值在哪个位置上
int minIndex = i;
for(int j = i + 1; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr, i, minIndex);
}
}
4. 插入排序
/**
* 简单插入排序:平均时间复杂度O(n²),最坏时间复杂度O(n²),最好时间复杂度O(n),空间O(1),稳定
* 1. 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
* 2. 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。
* (如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
*/
private void insertSort(int[] array) {
int len = array.length;
for (int i = 1; i < len; i++) {
int temp = array[i]; // 记录要插入的数据
// 从已经排序的序列最右边的开始比较,找到比其小的数
int j = i;
while (j > 0 && array[j - 1] > temp) {
array[j] = array[j - 1];
j--;
}
// 存在比其小的数,插入
if (j != i) {
array[j] = temp;
}
}
}
或如下方式实现:
void insertSort(int[] arr) {
for(int i = 1; i < arr.length; i++) {
for(int j = i - 1; j >= 0 && arr[j] > arr[j+1]; j--) {
swap(arr, j, j + 1);
}
}
}
5. 归并排序
/**
* 归并排序:采用分治法 平均时间复杂度O(nlogn) ,最坏时间复杂度O(nlogn) ,最好时间复杂度O(nlogn) ,空间O(n),稳定
* 1.申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
* 2.设定两个指针,最初位置分别为两个已经排序序列的起始位置;
* 3.比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
* 4.重复步骤 3 直到某一指针达到序列尾;
* 5.将另一序列剩下的所有元素直接复制到合并序列尾。
*/
private int[] mergeSort(int[] array) {
int middle = (int) Math.floor(array.length / 2);
int[] left = Arrays.copyOfRange(array, 0, middle);
int[] right = Arrays.copyOfRange(array, middle, array.length);
return merge(mergeSort(left), mergeSort(right));
}
private int[] merge(int[] left, int[] right) {
int[] res = new int[left.length + right.length];
int i = 0;
while (left.length > 0 && right.length > 0) {
if (left[0] <= right[0]) {
res[i++] = left[0];
left = Arrays.copyOfRange(left, 1, left.length);
} else {
res[i++] = right[0];
right = Arrays.copyOfRange(right, 1, right.length);
}
}
while (left.length > 0) {
res[i++] = left[0];
left = Arrays.copyOfRange(left, 1, left.length);
}
while (right.length > 0) {
res[i++] = right[0];
right = Arrays.copyOfRange(right, 1, right.length);
}
return res;
}
或用如下方式实现:
// 通过递归实现
public void mergeSort(int[] arr) {
if(arr == null || arr.length < 2) return;
process(arr, 0, arr.length - 1);
}
// arr[L..R] 范围上,变成有序的
private void process(int[] arr, int L, int R) {
if(L == R) return;
int mid = L + ((R - L) >> 1);
process(arr, L, mid);
process(arr, mid + 1, R);
merge(arr, L, mid, R);
}
private void merge(int[] arr, int L, int M, int R) {
// L..R 范围的大小
int[] help = new int[R - L + 1];
int i = 0;
int p1 = L;
int p2 = M + 1;
while(p1 <= M && p2 <= R) {
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while(p1 <= M) {
help[i++] = arr[p1++];
}
while(p2 <= R) {
help[i++] = arr[p2++];
}
for(i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
非递归方法实现:
public void mergerSort(int[] arr) {
if (arr == null || arr.length < 2) return;
int N = arr.length;
int mergeSize = 1;// 当前有序的左组长度,一组是2倍的mergeSize
while(mergeSize < N) {
int L = 0;
while(L < N) {
int M = L + mergeSize - 1; // L..M
if(M >= N) break;
int R = Math.min(M + mergeSize, N - 1); // L..M M+1..R
merge(arr, L, M, R);
L = R + 1;
}
if(mergeSize > N / 2) break; // 防止溢出
mergeSize <<= 1; // mergeSize * 2
}
}
6. 快速排序
/**
* 快速排序:平均时间复杂度O(nlogn) ,最坏时间复杂度O(n²) ,最好时间复杂度O(nlogn) ,空间O(logn),不稳定
* 1.从数列中挑出一个元素,称为 "基准"(pivot);
* 2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
* 在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
* 3.递归地把小于基准值元素的子数列和大于基准值元素的子数列排序;
*/
private int[] quickSort(int[] array, int left, int right) {
if (left < right) {
int partitionIndex = partition(array, left, right);
quickSort(array, left, partitionIndex - 1);
quickSort(array, partitionIndex + 1, right);
}
return array;
}
// 分治(单边循环法)确定基准元素
private int partition(int[] arr, int left, int right) {
// 设定基准值
int pivot = left;
int index = pivot + 1;
for (int i = index; i <= right; i++) {
if (arr[i] < arr[pivot]) {
swap(arr, i, index);
index++;
}
}
swap(arr, pivot, index - 1);
return index - 1;
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
或用如下方式实现:
// 以 arr[R] 为划分值
// [ <=arr[R] arr[R] >arr[R] ]
public static int partition(int[] arr, int L, int R) {
if (L > R) return -1;
if (L == R) return L;
int lessEqual = L - 1;
int index = L;
while (index < R) {
if (arr[index] <= arr[R]) {
swap(arr, index, ++lessEqual);
}
index++;
}
swap(arr, ++lessEqual, R);
return lessEqual;
}
/**
* 快排
* 时间复杂度:最差 O(N^2)
*/
public static void quickSort1(int[] arr) {
if (arr == null || arr.length < 2) return;
process1(arr, 0, arr.length - 1);
}
public static void process1(int[] arr, int L, int R) {
if (L > R) return;
int M = partition(arr, L, R);
process1(arr, L, M - 1);
process1(arr, M + 1, R);
}
或利用荷兰国旗问题:
// 在 arr[L..R] 玩荷兰国旗问题的划分,以 arr[R] 做划分值
// <arr[R] ==arr[R] >arr[R]
// 返回等于区域的左右边界位置
// 如 [1,2, 3,3, 5,4] 返回3的区域左右边界下标:[2,3]
public static int[] netherlandsFlag(int[] arr, int L, int R) {
if (L > R) return new int[]{-1, -1};
if (L == R) return new int[]{L, R}; // 说明区域只有一个数
int less = L - 1; // 小于区域右边界
int more = R; // 大于区域左边界
int index = L;
while (index < more) {
if (arr[index] == arr[R]) {
index++;
} else if (arr[index] < arr[R]) {
// 把当前位置和小于区域做交换,并右移增加小于区域右边界
// 同时下标index++
swap(arr, index++, ++less);
} else {
// 把当前位置和大于区域做交换,并左移扩大大于区域左边界
// index不变
swap(arr, index, --more);
}
}
// 此时:L..less less+1..more-1 more...R-1 R
// 最后把划分值 arr[R] 和大于区域的左边界交换,使得划分值在 ==arr[R] 右边相邻
swap(arr, more, R);
return new int[]{less + 1, more};
}
public static void quickSort2(int[] arr) {
if (arr == null || arr.length < 2) return;
process2(arr, 0, arr.length - 1);
}
public static void process2(int[] arr, int L, int R) {
if (L > R) return;
// 中间相等的区域不需要排了
int[] equalArea = netherlandsFlag(arr, L, R);
process2(arr, L, equalArea[0] - 1);
process2(arr, equalArea[1] + 1, R);
}
经典随机快排如下:
/**
* 经典随机快排
* 时间复杂度: O(NlogN)
* 空间复杂度: O(logN)
*/
public static void quickSort3(int[] arr) {
if (arr == null || arr.length < 2) return;
process3(arr, 0, arr.length - 1);
}
public static void process3(int[] arr, int L, int R) {
if (L > R) return;
// 随机选个位置,不以 arr[R] 为划分值
swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
// 中间相等的区域不需要排了
int[] equalArea = netherlandsFlag(arr, L, R);
process3(arr, L, equalArea[0] - 1);
process3(arr, equalArea[1] + 1, R);
}
7. 堆排序
/**
* 堆排序:平均时间复杂度O(nlogn) ,最坏时间复杂度O(nlogn) ,最好时间复杂度O(nlogn) ,空间O(1),不稳定
* <p>
* 堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。
* 堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点.
* <p>
* 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
* 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
* <p>
* 1.创建一个堆 H[0……n-1];
* 2.把堆首(最大值)和堆尾互换;
* 3.把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
* 4.重复步骤 2,直到堆的尺寸为 1
*/
private void heapSort(int[] array) {
int len = array.length;
buildMapHeap(array, len);
for (int i = len - 1; i > 0; i--) {
swap(array, 0, i);
len--;
heapify(array, 0, len);
}
}
private void buildMapHeap(int[] arr, int len) {
for (int i = (int) Math.floor(len / 2); i >= 0; i--) {
heapify(arr, i, len);
}
}
// 堆化
private void heapify(int[] arr, int i, int len) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int largest = i;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest, len);
}
}
8. 计数排序
/**
* 计数排序:平均时间复杂度O(n+k) ,最坏时间复杂度O(n+k) ,最好时间复杂度O(n+k) ,空间O(n+k),稳定
* <p>
* 核心是将输入的数据值转化为键存储在额外开辟的数组空间中。
* <p>
* 1.找出待排序的数组中最大和最小的元素
* 2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项
* 3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
* 4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
*/
private void countingSort(int[] array) {
int maxValue = getMaxValue(array);
int bucketLen = maxValue + 1; // 计数桶长度
int[] bucket = new int[bucketLen];
// 初始化计数桶
for (int value : array) {
bucket[value]++;
}
int sortedIndex = 0;
for (int j = 0; j < bucketLen; j++) {
while (bucket[j] > 0) {
array[sortedIndex++] = j;
bucket[j]--;
}
}
}
private int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}
9. 桶排序
/**
* 桶排序:平均时间复杂度O(n+k) ,最坏时间复杂度O(n²) ,最好时间复杂度O(n) ,空间O(n+k),稳定
* <p>
* 计数排序的升级版。
* 它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定.
* <p>
* 1.在额外空间充足的情况下,尽量增大桶的数量
* 2.使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
*/
private void bucketSort(int[] array) {
int bucketSize = 5;
// 寻找最大最小数
int minValue = array[0];
int maxValue = array[0];
for (int value : array) {
if (value < minValue) {
maxValue = value;
} else if (value > maxValue) {
maxValue = value;
}
}
// 初始化桶
int bucketCount = (int) Math.floor((maxValue - maxValue) / bucketSize) + 1;
int[][] buckets = new int[bucketCount][0];
// 利用映射函数将数据分配到各个桶中
for (int i = 0; i < array.length; i++) {
int index = (int) Math.floor((array[i] - minValue) / bucketSize);
buckets[index] = arrAppend(buckets[index], array[i]);
}
int arrIndex = 0;
for (int[] bucket : buckets) {
if (bucket.length <= 0) continue;
// 对每个桶进行排序,这边排序可用前面介绍过的排序算法
insertSort(bucket);
for (int value : bucket) {
array[arrIndex++] = value;
}
}
}
/**
* 自动扩容,并保存数据
*/
private int[] arrAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
参考链接: