写在前面:本文所讲排序算法是指七大基本排序
1 排序的概念及分类:
1.1排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持
不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
1.2排序的分类
内部排序:数据元素全部放在内存中的排序
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
1.3常见的排序算法
2 排序算法的实现
2.1插入排序
原理:类似于扑克整理牌的过程,将待排序的值按其关键码值的大小依次插入到有序区间内。
2.1.1排序性能
时间复杂度:O(n^2)
空间复杂度: O(1)
稳定性: 稳定
2.1.2 代码实现:
public static void insert(long[] array) {
for (int i = 1; i < array.length; i++) { // 循环 n - 1 次
// 有序区间: [0, i)
// 无序区间: [i, array.length)
long x = array[i];
int j;
for (j = i - 1; j >= 0 && array[j] > x; j--) {
array[j + 1] = array[j];
}
array[j + 1] = x;
}
}
2.2 选择排序
2.2.1 基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
2.2.2 排序性能
时间复杂度:O(n^2)
空间复杂度: O(1)
稳定性: 不稳定
2.2.3 代码实现
public static void selectSort(long[] array) {
for (int i = 0; i < array.length - 1; i++) { // 一共进行 n - 1 次选择过程
// [无序区间] [有序区间]
// 无序区间: [0, array.length - i)
// 有序区间: [array.length - i, array.length)
// 1. 遍历整个无序区间,找到最大的元素所在的下标(不需要知道最大的元素是多少,而是它在哪)
int maxIdx = 0;
// 在剩余位置,继续找比 array[maxIdx] 元素还大的元素
for (int j = 1; j < array.length - i; j++) {
// 期间,如果出现更的元素,则更新最大元素所在下标
if (array[j] > array[maxIdx]) {
maxIdx = j;
}
}
// 最大的元素在 [maxIdx],即 array[maxIdx] 是无序区间的最大元素
// 把最大的元素放到无序区间的最后一个位置
// [0, array.length - i)
// 无序区间的最后一个位置下标是 [array.length - i - 1]
swap(array, maxIdx, array.length - i - 1);
}
}
private static void swap(long[] array,int a,int b){
long tmp = array[a];
array[a] = array[b];
array[b] = tmp;
}
2.3冒泡排序
2.3.1 排序思想
冒泡排序可以说是一种非常经典的排序算法,常被用于教学示例,非常便于理解
原理:就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动
2.3.2 排序性能
时间复杂度:O(n^2)
空间复杂度: O(1)
稳定性: 稳定
2.3.3 代码实现
public static void bubbleSort(long[] array){
for (int i = 0; i < array.length - 1; i++) {
boolean sorted = true;
for (int j = 0; j < array.length - i -1; j++) {
if(array[j] > array[j+1]){
swap(array,j,j+1);
sorted = false;
}
}
if(sorted){
return;
}
}
}
上述三个排序的时间复杂度都是为O(n^2)的,下面我们介绍几个时间复杂度小一些的排序。
2.4希尔排序
2.4.1 排序原理
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序本质上是对直接插入排序的优化
2.4.2 排序性能
时间复杂度:希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,平均在O(n^1.3)左右
空间复杂度: O(1)
稳定性:不稳定
2.4.3 代码实现
public static void shellSort(long[] array) {
int gap = array.length / 2;
while (true) {
// 带有间隔的分组插排
for (int i = gap; i < array.length; i++) {
// x是后一组的
long x = array[i];
int j;
for (j = i - gap; j >= 0 && array[j] > x; j = j - gap) {
array[j + gap] = array[j];
}
//如果前一组的小于后一组的,不用动
array[j + gap] = x;
}
if (gap == 1) {
// 说明上一次的分组插排其实最终的插排
break;
}
gap = gap / 2;
}
}
2.5 堆排序
2.5.1 排序原理
堆排序(Heapsort):是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
2.5.2 排序性能
时间复杂度:O(N*logN)
空间复杂度:O(1)
稳定性:不稳定
2.5.3代码实现
public static void adjustDownbig(long[] array,int size,int index){
while(2 * index + 1 < size){
int maxIdx = 2 * index + 1;
if(maxIdx + 1 < size && array[maxIdx + 1] > array[maxIdx]){
maxIdx++;
}
if(array[index] > array[maxIdx]){
return;
}
swap(array,maxIdx,index);
index = maxIdx;
}
}
public static void creatBigHeap(long[] array,int size){
int pIdx = (size - 2)/2;
for (int i = pIdx; i >= 0 ; i--) {
adjustDownbig(array,size,i);
}
}
public static void heapSort(long[] array){
creatBigHeap(array,array.length);
for (int i = 0; i < array.length - 1; i++) {
swap(array,array.length - i - 1,0);
adjustDownbig(array,array.length - i - 1,0);
}
}
2.6 快速排序
2.6.1排序原理
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int[] array, int left, int right)
{
if(right - left <= 1)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int div = partion(array, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
// 递归排[left, div)
QuickSort(array, left, div);
// 递归排[div+1, right)
QuickSort(array, div+1, right);
}
上述代码为快速排序的主框架,其中的partion方法不止一种,常见的有 Hoare,挖坑,前后指针方法。
2.6.2 排序性能
时间复杂度:O(N*logN)
空间复杂度:O(logN)
稳定性:不稳定
2.6.3 代码实现
不用版本的代码只有paetion方法不同,其余框架无需改动
Hoare版本
public static void quickSort(long[] array) {
quickSortInternal(array, 0, array.length - 1); // 左闭右闭区间
}
// 左闭右闭:[from, to]
private static void quickSortInternal(long[] array, int from, int to) {
// 本次待排序区间的元素个数: to - from + 1
if (to - from + 1 <= 1) {
// 待排序区间内的元素个数 <= 1
// 停止处理这个区间
return;
}
// 选择区间最右边的元素作为 pivot
// 最右边元素的下标 [to]
// 进行 partition 的操作
// 返回值是最终 pivot 的所在下标
int i = partitionDigHole(array, from, to);
// 通过 i 这个下标,我们将 [from, to] 这个区间切割成两份
// 左边的 [from, i - 1] 这里的元素都是 <= pivot
// 右边的 [i + 1, to] 这里的元素都是 >= pivot
// 把左右两个小区间按照相同的方式进行处理
quickSortInternal(array, from, i - 1);
quickSortInternal(array, i + 1, to);
}
private static int partitionHoare(long[] array, int from, int to) {
long pivot = array[to]; // 选择区间的最右边的元素作为基准值
int left = from;
int right = to;
// 当 [left, right) 区间内还有元素时,循环,得继续
while (left < right) { // left < right 只在此处判断了
// 当选择最右边作为基准值时,优先从左边开始,否则,某些情况下会有元素没有和pivot比过
while (left < right && array[left] <= pivot) {
// 判断left < right 是为了防止left++ 导致left > right
left++; // left 一直在变,可能破坏 left < right 的约束
}
while (left < right && array[right] >= pivot) {
right--; // right 一直在变,可能破坏 left < right 的约束
}
// array[left] > pivot && array[right] < pivot
swap(array, left, right);
}
swap(array, right, to);
return right;
}
挖坑版本
private static int partitionDigHole(long[] array, int from, int to) {
long pivot = array[to];
int left = from;
int right = to;
while (left < right) {
while(left < right && array[left] < pivot){
left++;
}
//right 指向的是末尾,首先要把末尾挖坑,把left传进去,此时left位置就是下一个新坑
array[right] = array[left];
while (left < right && array[right] > pivot){
right--;
}
// 此时left是新坑位置,right找到比pivot小的元素后,把right传到新坑去
array[left] = array[right];
}
array[left] = pivot;
return left;
}
前后指针版本:
private static int partition3(long[] array, int from, int to) {
/*
小于基准值的区间范围: [from, b)
大于等于基准值的区间范围: [b, d)
未比较的元素的区间范围: [d, to)
基准值 [to, to]
*/
long pivot = array[to];
// 最开始
// [from, b) => [from, from) 区间内一个元素都没有
// [b, d) => [from, from) 区间内一个元素都没有
// [d, to) => [from, to) 整个区间所有元素都在里面
int b = from;
for (int d = from; d < to; d++) {
if (array[d] < pivot) {
swap(array, b, d);
b++;
}
}
swap(array, b, to);
return b;
}
(前后指针方法还可以进行优化)
优化版本:
public static void 进阶版Partition(long[] array, int from, int to) {
long pivot = array[to];
/*
[from, b) 小于基准值
[b, d) 等于基准值
[d, g] 未比较的元素
(g, to] 大于基准值
*/
int b = from;
int d = from;
int g = to;
// [d, g] 这个区间内没有元素时停止; d <= g 说明还有元素
while (d <= g) {
if (array[d] == pivot) {
d++;
} else if (array[d] < pivot) {
swap(array, d, b);
b++;
d++;
} else {
// array[d] > pivot
swap(array, d, g);
g--;
}
}
}
2.7归并排序
2.7.1排序原理
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
具体步骤如下:
![](https://i-blog.csdnimg.cn/blog_migrate/ec3a8b25e8beff95ca61d9ed6daa832e.png)
2.7.2排序性能
1.归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
时间复杂度:O(N*logN)
空间复杂度:O(N)
稳定性:稳定
2.7.3代码实现
public static void mergeSort(long[] array) {
mergeSortInternal(array, 0, array.length);
}
// 待排序区间:[from, to)
private static void mergeSortInternal(long[] array, int from, int to) {
if (to <= 1 + from) {
return;
}
// 把待排序区间对半分开
// 找到中间位置下标
int mid = from + (to - from) / 2;
// 整个区间被切割成
// [from, mid)
// [mid, to)
// 按照相同的方式,先把左右两个区间变得有序
mergeSortInternal(array, from, mid);
mergeSortInternal(array, mid, to);
// [from, mid) 有序了
// [mid, to) 有序了
merge(array, from, mid, to);
}
private static void merge(long[] array, int from, int mid, int to) {
int size = to - from;
// 需要的额外空间
long[] e = new long[size];
int eIdx = 0;
int leftIdx = from; // 左边区间要处理的元素的下标
int rightIdx = mid; // 右边区间要处理的元素的下标
// 什么条件表示左边区间 [from, mid) 就没有元素了 leftIdx == mid
// 同理右边区间:rightIdx == to
// 两个区间都有元素的时候才要比较
// 左区间有元素:leftIdx < mid
// 右: rightIdx < to
// leftIdx < mid && rightIdx < to
while (leftIdx < mid && rightIdx < to) {
// 要比较的两个元素 array[leftIdx] 和 array[rightIdx]
if (array[leftIdx] <= array[rightIdx]) {
// 取左边的元素放入 e 数组
e[eIdx] = array[leftIdx];
eIdx++;
leftIdx++;
} else {
// 取右边的元素放入 e 数组
e[eIdx] = array[rightIdx];
eIdx++;
rightIdx++;
}
}
// 说明有一个区间的元素被全部取完了
if (leftIdx < mid) {//if语句可以省略
// 说明右边取完了但左区间还没取完,把左边的所有元素依次放到 e 中
while (leftIdx < mid) {
e[eIdx] = array[leftIdx];
eIdx++;
leftIdx++;
}
} else {
// 左区间元素取完了,但右区间还没取完
while (rightIdx < to) {
e[eIdx] = array[rightIdx];
eIdx++;
rightIdx++;
}
}
// e 里是有序的 [0, size)
// 要把有序的元素从 e 中搬回到 array 中 [from, to)
for (int i = 0; i < size; i++) {
array[from + i] = e[i];
}
}
总结