
排序是计算机科学中最基础且重要的主题之一,无论是学术研究还是实际开发,都离不开排序算法的应用。
本文将系统介绍常用且重要的排序算法,分析它们的性能特点。
排序是将一串记录按照某个或某些关键字的大小,递增或递减排列起来的操作。
简单来说,就是将一组无序的数据变成有序的过程
稳定性是排序算法的重要特性。
假设在待排序序列中存在多个相同关键字的记录,如果排序后这些记录的相对次序保持不变,则称该算法是稳定的;否则称为不稳定。
例如:序列 [9, 5a, 2, 7, 5b, 8] 经过稳定排序后,5a仍然在5b之前
因为比较排序算法离不开大小比较,因此小编先把交换方法swap写在前面:
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}图示如下:




// 直接插入排序
public static void insertSort (int[] arr) {
for (int i = 1; i < arr.length; i++) {
int temp = arr[i];
int j = i - 1;
for (; j >= 0; j--) {
if (arr[j] > temp)
arr[j+1] = arr[j];
else {
arr[j+1] = temp;
break;
}
}
arr[j+1] = temp;
}
}是对直接插入排序的优化
分组插入排序+缩小增量gap。
当gap>=1,属于预排序。
希尔排序采用跳跃式分组(按照gap进行分组),好处是能够把大的数据放到更靠后的位置,随着分的组数越来越少,数据逐渐趋于有序
图示如下:



// 希尔排序
public static void shellSort (int[] arr) {
// 让gap等于数据的长度
int gap = arr.length;
while (gap > 1) {
// 每次按照gap的一半进行分组
gap /= 2;
// 每组进行直接插入排序
shell(arr,gap);
}
}
private static void shell(int[] arr, int gap) {
for (int i = gap; i < arr.length; i++) {
int temp = arr[i];
int j = i - gap;
for (; j >= 0; j -= gap) {
if (arr[j] > temp)
arr[j+gap] = arr[j];
else {
arr[j+gap] = temp;
break;
}
}
arr[j+gap] = temp;
}
}图示如下:



// 直接选择排序
public static void selectSort2 (int[] arr) {
for (int i = 0; i < arr.length; i++) {
// 默认第i个数据最小
int minIndex = i;
for (int j = i+1; j < arr.length; j++) {
// 进行比较,找到比min还小的数
if (arr[j] < arr[minIndex])
minIndex = j;
}
// 执行到这里时,已经找到/或者没有更小的数
// 进行交换操作
swap(arr,i,minIndex);
}
}图示如下:





// 优化版
public static void selectSort (int[] arr) {
int left = 0;
int right = arr.length - 1;
while (left < right) {
// 默认left所在位置的值是最小值和最大值
int minIndex = left;
int maxIndex = left;
// 找到数据中的最小值和最大值
for (int i = left+1; i <= right; i++) {
if (arr[i] < arr[minIndex])
minIndex = i;
if (arr[i] > arr[maxIndex])
maxIndex = i;
}
// 把最小值放到第一个位置
swap(arr,left,minIndex);
// 当数据的第一个数就是最大值时,由于先换了最小值,所以此时第一个数在minIndex位置
if (maxIndex == left)
maxIndex = minIndex;
// 把最大值放到最后位置
swap(arr,right,maxIndex);
left++;
right--;
}
}升序为例:
图示如下:





// 堆排序
public static void heapSort (int[] arr) {
// 创建堆
createHeap(arr);
// 标记的最后元素的位置,每一次调整后都要减一
int end = arr.length - 1;
while (end > 0) {
// 将第一个元素和最后元素交换
swap(arr,0,end);
// 向下调整以第一个元素为堆顶的堆
shiftDown(arr,0,end);
// 标记最后元素位置的end自减1
end--;
}
}
private static void createHeap(int[] arr) {
for (int parent = (arr.length-2)/2; parent >= 0; parent--) {
// 向下调整建堆
shiftDown(arr,parent,arr.length);
}
}
private static void shiftDown(int[] arr, int parent, int length) {
// param: 目标数据 起始范围 结束范围
int child = parent * 2 + 1;
while (child < length) {
// 找到较大的数:确保下标位置合法
if ((child+1)<length && arr[child]<arr[child+1]) {
child++;
}
// 与parent的值比较
if (arr[child] > arr[parent]) {
swap(arr,child,parent);
// 往子树走
parent = child;
child = parent * 2 + 1;
} else {
break;
}
}
}图示如下:




// 冒泡排序
public static void bubbleSort2 (int[] arr) {
// i控制比较的趟数
for (int i = 0; i < arr.length-1; i++) {
// j控制每一趟比较的次数
for (int j = 0; j < arr.length-1-i; j++) {
if (arr[j] > arr[j+1]) {
swap(arr,j,j+1);
}
}
}
}// 优化版
public static void bubbleSort (int[] arr) {
// i控制比较的趟数
for (int i = 0; i < arr.length-1; i++) {
// 每一次排序时都默认标记有序,表示数据是有序的
boolean isOrder = true;
// j控制每一趟比较的次数
for (int j = 0; j < arr.length-1-i; j++) {
if (arr[j] > arr[j+1]) {
swap(arr,j,j+1);
// 一旦交换一次,就改变标记,表示数据是无序的
isOrder = false;
}
}
// 若标记始终有序,就直接退出比较
if (isOrder) {
break;
}
}
}有三种划分方法:
小编在这里逐一配图来给读者解析
图示如下:







// hoare法
public static void quickSortHoare (int[] arr) {
quickHoare(arr,0,arr.length-1);
}
private static void quickHoare(int[] arr, int start, int end) {
// 当范围不合法就退出
if (start >= end)
return;
// 将数据以基准值划分
int pivot = patitionHoare(arr,start,end);
// 递归
quickHoare(arr,start,pivot-1);
quickHoare(arr,pivot+1,end);
}
private static int patitionHoare (int[] arr, int left, int right) {
// 基准值
int base = arr[left];
int baseIndex = left;
// 当两个引用还没相遇时进行操作
while (left < right) {
// 若值没有基准值小
if (left<right && arr[right]>=base) {
right--;
}
// 若值没有基准值大
if (left<right && arr[left]<=base) {
left++;
}
// 交换min和max的值
swap(arr,left,right);
}
// 当两个引用相遇时,将基准值与相遇位置的值交换
swap(arr,baseIndex,left);
return left;
}图示如下:




// 挖坑法
private static int patition (int[] arr, int left, int right) {
// 把基准值暂存至temp
int temp = arr[left];
// 当两个引用还没相遇时进行操作
while (left < right) {
// 若值没有基准值小
while (left<right && arr[right]>=temp) {
right--;
}
// 将最小的数放到left位置
arr[left] = arr[right];
// 若值没有基准值大
while (left<right && arr[left]<=temp) {
left++;
}
// 将最大的数放到right位置
arr[right] = arr[left];
}
// 当两个引用相遇时,将基准值放到相遇位置
arr[left] = temp;
return left;
}图示如下:





// 前后指针法
private static int patitionPCPtr (int[] arr, int left, int right) {
// 定义两个引用
int prev = left;
int cur = left + 1;
// 合法范围内进行操作
while (cur <= right) {
// 找到比基准值大的数
if (arr[cur]<arr[left] && arr[++prev]!=arr[cur]) {
swap(arr,prev,cur);
}
cur++;
}
// 将prev的值和基准值交换
swap(arr,prev,left);
return prev;
}由于快速排序是递归进行的,当数据量过大时,不断递归可能会导致栈溢出。
因此,需要优化递归——减少递归的次数。
有两种方法可以减少递归的次数:
三数取中法:
如图:

优化后的完整快速排序算法:
public static void quickSort (int[] arr) {
quick(arr,0,arr.length-1);
}
private static void quick(int[] arr, int start, int end) {
// 当范围不合法就退出
if (start >= end)
return;
// 递归到小范围的数据时,使用直接插入排序,减少递归的次数
if (end-start+1 <= 7) {
insertSortRange(arr,start,end);
return;
}
// 三数取中找中位数并以中位数位基准值
int midIndex = getMiddleNum(arr,start,end);
swap(arr,start,midIndex);
// 将数据以基准值划分
int pivot = patition(arr,start,end);
// 递归
quick(arr,start,pivot-1);
quick(arr,pivot+1,end);
}
private static int patition (int[] arr, int left, int right) {
// 把基准值暂存至temp
int temp = arr[left];
// 当两个引用还没相遇时进行操作
while (left < right) {
// 若值没有基准值小
while (left<right && arr[right]>=temp) {
right--;
}
// 将最小的数放到left位置
arr[left] = arr[right];
// 若值没有基准值大
while (left<right && arr[left]<=temp) {
left++;
}
// 将最大的数放到right位置
arr[right] = arr[left];
}
// 当两个引用相遇时,将基准值放到相遇位置
arr[left] = temp;
return left;
}
// 三数取中法
private static int getMiddleNum(int[] arr, int start, int end) {
int mid = (start + end) / 2;
if (arr[start] < arr[end]) {
if (arr[mid] < arr[start]) {
return start;
} else if (arr[mid] > arr[end]) {
return end;
} else {
return mid;
}
} else {
if (arr[mid] > arr[start]) {
return start;
} else if (arr[mid] < arr[end]) {
return end;
} else {
return mid;
}
}
}
// 直接插入法(针对一定范围)
private static void insertSortRange (int[] arr, int start, int end) {
for (int i = start+1; i <= end; i++) {
int temp = arr[i];
int j = i - 1;
for (; j >= start; j--) {
if (arr[j] > temp)
arr[j+1] = arr[j];
else {
arr[j+1] = temp;
break;
}
}
arr[j+1] = temp;
}
}图示如下:




// 非递归快速排序
public static void quickSortNonTra (int[] arr) {
quickNonTra(arr,0,arr.length-1);
}
private static void quickNonTra(int[] arr, int start, int end) {
Stack<Integer> stack = new Stack<>();
// 找到基准值
int pivot = patition(arr,start,end);
// 若基准值的左边/右边至少有两个元素,就需要排序,故入栈
if (pivot > start+1) {
stack.push(start);
stack.push(pivot-1);
}
if (pivot < end-1) {
stack.push(pivot+1);
stack.push(end);
}
// 当栈不为空就持续排序
while (!stack.isEmpty()) {
// 弹出的第一个元素是end,第二个元素是start
end = stack.pop();
start = stack.pop();
// 找到基准值
pivot = patition(arr,start,end);
// 若基准值的左边/右边至少有两个元素,就需要排序,故入栈
if (pivot > start+1) {
stack.push(start);
stack.push(pivot-1);
}
if (pivot < end-1) {
stack.push(pivot+1);
stack.push(end);
}
}
}图示如下:









合并的具体操作详见链表面试题中的:合并两个有序链表,思路是一样的。
// 归并排序
public static void mergeSort (int[] arr) {
splitAndMerge(arr,0,arr.length-1);
}
private static void splitAndMerge(int[] arr, int left, int right) {
// 分解
if (left >= right)
return;
int mid = (left + right) / 2;
splitAndMerge(arr,left,mid);
splitAndMerge(arr,mid+1,right);
// 合并
merge(arr,left,mid,right);
}
private static void merge(int[] arr, int left, int mid, int right) {
int[] ret = new int[arr.length];
int k = 0;
int fs = left;
//int fe = mid;
int ls = mid + 1;
//int le = right;
// 当两个有序数组都不为空
while (fs<=mid && ls<=right) {
if (arr[fs] < arr[ls]) {
ret[k++] = arr[fs++];
} else {
ret[k++] = arr[ls++];
}
}
while (fs <= mid) {
ret[k++] = arr[fs++];
}
while (ls <= right) {
ret[k++] = arr[ls++];
}
// 此时ret数组已经存入全部数据,将ret数组接入arr数组
for (int i = 0; i < k; i++) {
arr[i+left] = ret[i];
}
}图示如下:




合并操作传送:合并两个有序链表
// 非递归归并排序
public static void mergeSortNonTra (int[] arr) {
// 最开始每一个元素看成一组有序的数组
int gap = 1;
// 确保gap不超过数组长度
while (gap < arr.length) {
// 遍历数据,每次走i+2*gap步
for (int i = 0; i < arr.length; i = i+2*gap) {
int left = i;
int mid = left + gap - 1;
// 判断mid是否合法
if (mid >= arr.length) {
mid = arr.length - 1;
}
int right = mid + gap;
// 判断right是否合法
if (right >= arr.length) {
right = arr.length - 1;
}
// 合并数组
merge(arr,left,mid,right);
}
// 每一轮结束后让gap乘2
gap *= 2;
}
}图示如下:




// 计数排序(非基于比较的排序)
public static void countSort (int[] arr) {
// 1.找数据中的最大值和最小值,然后确定 计数数组 的长度
int minVal = arr[0];
int maxVal = arr[0];
for (int i = 0; i < arr.length; i++) {
if (arr[i] < minVal) {
minVal = arr[i];
}
if (arr[i] > maxVal) {
maxVal = arr[i];
}
}
// 计算 计数数组 的长度
int len = maxVal - minVal + 1;
int[] count = new int[len];
// 2.遍历原数组,通过 计数数组 统计每一个数个位数字出现的个数
for (int i = 0; i < arr.length; i++) {
count[arr[i]-minVal]++;
}
// 3.遍历计数数组,按顺序覆盖原数组
int index = 0;
for (int i = 0; i < count.length; i++) {
while (count[i] != 0) {
arr[index++] = i + minVal;
count[i]--;
}
}
}至此,八大排序就全部讲解完毕啦,若有不正确的,请尽管指出!
希望读者朋友们能够学到知识~
完