Gitee:gitee.com/zhangziyi11…
记待排序的数组为 int[] arr,数组长度为 n。升序排序。
排序算法大致可分两类:基于比较的排序和基于收集的排序。基于比较的排序属于原地排序,时间复杂度无法突破,基于收集的排序使用额外空间辅助,时间复杂度可以继续突破。
1. 选择排序
算法思想:对前 n-1个位置,选择其后的最小者,将其交换到该位置。
时间复杂度:
稳定性:不是两两相邻元素比较,因此不稳定。
public void sort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
// 此轮循环中最小值和最小值下标
int min = arr[i], minIndex = i;
for (int j = i; j < arr.length; j++) {
if (arr[j] < min) {
min = arr[j];
minIndex = j;
}
}
swap(arr, i, minIndex);
}
}
2. 冒泡排序
算法思想:对后 n-1 个位置,将最大的数两两比较移动至此,具体做法是:两两比较,大者右移,像一个水中的气泡不断上浮。
时间复杂度:。
稳定性:相邻元素两两比较,是稳定的。
public void sort(int[] arr) {
for (int i = arr.length - 1; i > 0; i--) {
for (int j = 1; j <= i; j++) {
if (arr[j - 1] > arr[j]) {
swap(arr, j, j - 1);
}
}
}
}
3. 插入排序
算法思想:初始情况下,将首个元素视为排好序的序列,从第二个元素开始,不断插入并形成排好序的序列。
时间复杂度:。
稳定性:由于是两两比较交换,因此是稳定的。
public void sort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
// 将 arr[i] 插入 [0,...,i-1],方法是不断比较并交换
for (int j = i; j > 0; j--) {
if (arr[j] < arr[j - 1]) {
swap(arr, j, j - 1);
} else {
break;
}
}
}
}
4. 希尔排序
算法思想:也叫缩小增量排序,是插入排序的变体。选取一个缩小增量 ti 序列,如 [len/2, len/4, ... ,1],对于每个增量 ti,对这 ti 个子序列使用插入排序。最后当 ti=1 时,便是对整个序列排序。
时间复杂度:平均为。
稳定性:在大于 1 的序列进行插入排序时,并非两两相邻比较,非稳定。
public class ShellSorter implements Sorter{
@Override
public void sort(int[] arr) {
for (int t = arr.length / 2; t >= 1; t /= 2) { // 对每个增量
for (int i = 0; i < t; i++) { // 对每个子序列
generalInsertSort(i, t, arr);
}
}
}
private void generalInsertSort(int startIndex, int incr, int[] arr) {
for (int i = startIndex + incr; i < arr.length; i += incr) {
for (int j = i; j > startIndex; j -= incr) {
if (arr[j] < arr[j - incr]) {
swap(arr, j, j - incr);
} else {
break;
}
}
}
}
}
5. 归并排序
算法思想:使用分治思想,将序列分成两个子序列,分别进行归并排序,再使用双指针进行合并。
时间复杂度:。
稳定性:指定优先选取前半序列的元素,因此是稳定的。
@Override
public void sort(int[] arr) {
mergeSort(arr);
}
private void mergeSort(int[] arr) {
if (arr.length == 1) {
return;
}
// 分割点
int splitPoint = arr.length / 2;
int[] firstHalf = Arrays.copyOfRange(arr, 0, splitPoint);// 前半序列
int[] secondHalf = Arrays.copyOfRange(arr, splitPoint, arr.length); // 后半序列
mergeSort(firstHalf);
mergeSort(secondHalf);
merge(arr, firstHalf, secondHalf);
}
private void merge(int[] arr, int[] firstHalf, int[] secondHalf) {
// 双指针,分别指向前半序列和后半序列
int p1 = 0, p2 = 0;
int p = 0; // 指向排序序列
while (p1 < firstHalf.length && p2 < secondHalf.length) {
if (firstHalf[p1] <= secondHalf[p2]) { // 相等时取前半部分的,保持稳定性
arr[p] = firstHalf[p1];
p1++;
} else {
arr[p] = secondHalf[p2];
p2++;
}
p++;
}
// 将剩余部分全部添加到arr
while (p1 < firstHalf.length) {
arr[p] = firstHalf[p1];
p++;
p1++;
}
while (p2 < secondHalf.length) {
arr[p] = secondHalf[p2];
p++;
p2++;
}
}
6. 快速排序
基本思想:将某个元素放置到最终位置,再递归地对左右子序列应用快速排序。
时间复杂度:。
稳定性:并非元素两两比较交换,因此不稳定。
@Override
public void sort(int[] arr) {
quickSort(arr, 0, arr.length);
}
/**
* 快速排序
* @param arr 排序数组
* @param startInclusive 起始下标(包含)
* @param endExclusive 终止下标(不包含)
*/
private void quickSort(int[] arr, int startInclusive, int endExclusive) {
if (startInclusive < endExclusive) {
int pivotIndex = layDownPivot(arr, startInclusive, endExclusive);
quickSort(arr, startInclusive, pivotIndex);
quickSort(arr, pivotIndex + 1, endExclusive);
}
}
/**
* 将最后一个元素视作枢轴元素,找到第一个大于枢轴元素的元素及其下标index,如果右侧没有更小的元素,则index就是枢轴最终的下标。
* 遍历index右侧的元素,更小的元素与index交换,index也右移,表示更小元素移到枢轴元素右侧。
* @param arr 排序数组
* @param startInclusive 起始下标(包含)
* @param endExclusive 终止下标(不包含)
*/
private int layDownPivot(int[] arr, int startInclusive, int endExclusive) {
int pivot = arr[endExclusive - 1]; // 枢轴元素
// 找到首个大于枢轴的元素下标
int p = findFirstBigger(arr, startInclusive, endExclusive, pivot);
if (p == -1) { // 此时表示枢轴元素已经在最终位置,可以返回了
return endExclusive - 1;
}
int pointer = p++; // 若右侧没有更小的元素,则pointer就是枢轴最终的下标
// 遍历剩余的元素,若找到小于枢轴的元素,将其与 pointer 交换,并让 pointer++
while (p < endExclusive) {
if (arr[p] < pivot) {
swap(arr, p, pointer);
pointer++;
}
p++;
}
// 让枢轴元素前往指定位置
swap(arr, pointer, endExclusive - 1);
return pointer;
}
/**
* 找到首个大于枢轴的元素下标
*/
private int findFirstBigger(int[] arr, int startInclusive, int endExclusive, int pivot) {
for (int i = startInclusive; i < endExclusive; i++) {
if (arr[i] > pivot) {
return i;
}
}
return -1;
}
7. 堆排序
算法思想:首先,在 [0,...,len-1] 范围内建立大根堆,将 arr[0] 与 arr[len-1] 交换,再在 [0,...,len-2] 范围内建立大根堆,直到 [0,1]。
时间复杂度:
稳定性:由于是跳跃比较,并非两两相邻比较,因此是非稳定的。
@Override
public void sort(int[] arr) {
heapSort(arr);
}
private void heapSort(int[] arr) {
for (int i = arr.length; i > 0; i--) {
// 建立[0,i)范围内的大根堆,则下标 0 处的结点值是最大的
buildMaxHeap(arr, i);
// 将最大值交换到最右侧
swap(arr, 0, i - 1);
}
}
/**
* 在[0,endExclusive)范围建立大根堆
* 从最后一个非叶子结点开始向左、向上遍历,看其是否为大根堆,若不是,把大的交换上来,交换下去的结点就不一定能当大根了,需要再比较交换,递归地进行下去。
*
*/
private void buildMaxHeap(int[] arr, int endExclusive) {
// 最后一个非叶子结点
int lastNotLeafNode = endExclusive / 2 - 1;
for (int i = lastNotLeafNode; i >= 0; i--) {
compareAndSwap(i, arr, endExclusive);
}
}
/**
* 比较并交换 index 及其左右孩子,若进行了交换,则递归地进行下去
*/
private void compareAndSwap(int index, int[] arr, int endExclusive) {
int largestIndex = findLargestIndex(index, arr, endExclusive); // 找到最大者下标
if (index != largestIndex) { // 父结点值不是最大的,需要交换
swap(arr, index, largestIndex);
compareAndSwap(largestIndex, arr, endExclusive);
}
}
/**
* 寻找下标为 defaultIndex 的结点和其左右孩子中最大者的下标
* @param defaultIndex 父结点下标
* @param arr 数组
* @param endExclusive 范围(不包括)
*/
private int findLargestIndex(int defaultIndex, int[] arr, int endExclusive) {
int largestIndex = defaultIndex;
int leftChildIndex = 2 * defaultIndex + 1, rightChildIndex = 2 * defaultIndex + 2;
if (leftChildIndex < endExclusive && arr[leftChildIndex] > arr[largestIndex]) {
largestIndex = leftChildIndex;
}
if (rightChildIndex < endExclusive && arr[rightChildIndex] > arr[largestIndex]) {
largestIndex = rightChildIndex;
}
return largestIndex;
}
8. 计数排序
基本思想:适用于固定范围的整数排序。假设数组最大值为 max,最小值为 min,则创建大小为 max-min+1 的数组 countArr 进行收集操作。遍历原数组,对一个数 num,countArr[num-min]++。遍历 countArr,将收集到的数按顺序添加到 arr。
时间复杂度:找到最大最小值,收集,释放,因此时间复杂度O(n)。
稳定性:可以稳定。
@Override
public void sort(int[] arr) {
int[] minAndMax = findM(arr);
int min = minAndMax[0], max = minAndMax[1];
// 创建收集数组
int[] countArr = new int[max - min + 1];
for (int num : arr) {
countArr[num - min]++;
}
// 添加到 arr
int p = 0;
for (int i = 0; i < countArr.length; i++) {
while (countArr[i] > 0) {
arr[p++] = i + min;
countArr[i]--;
}
}
}
/**
* 查找最值
*/
private int[] findM(int[] arr) {
int[] minAndMax = new int[2];
int minIndex = 0, maxIndex = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] < arr[minIndex]) {
minIndex = i;
continue;
}
if (arr[i] > arr[maxIndex]) {
maxIndex = i;
}
}
minAndMax[0] = arr[minIndex];
minAndMax[1] = arr[maxIndex];
return minAndMax;
}
9. 桶排序
基本思想:将元素放入指定数目的桶中,对每个桶中的元素分别进行排序,最后进行合并。
时间复杂度:创建桶O(k),获取最大最小值O(n),向桶中添加元素O(n),各桶排序O(k * (n/k)log(n/k))=O(nlog(n/k)),倾倒O(n),因此复杂度O(k+n+nlog(n/k))。
稳定性:可以稳定。
@Override
public void sort(int[] arr) {
bucketSort(arr, 3);
}
private void bucketSort(int[] arr, int bucketSize) {
// 创建桶
ArrayList<Integer>[] bucket = initializeBucket(bucketSize);
// 计算桶容量和最小值
int[] boundaryAndMin = countBoundary(arr, bucketSize);
int boundary = boundaryAndMin[0], min = boundaryAndMin[1];
// 向桶中添加元素
fillBucket(arr, bucket, bucketSize, min, boundary);
// 桶中元素排序
sortEachBucket(bucket);
// 倾倒桶中元素到数组
dump(arr, bucket);
}
private ArrayList<Integer>[] initializeBucket(int bucketSize) {
ArrayList<Integer>[] bucket = new ArrayList[bucketSize];
for (int i = 0; i < bucketSize; i++) {
bucket[i] = new ArrayList<>();
}
return bucket;
}
private void dump(int[] arr, ArrayList<Integer>[] bucket) {
int i = 0;
for (ArrayList<Integer> list : bucket) {
for (Integer num : list) {
arr[i++] = num;
}
}
}
/**
* 计算桶容量
*/
private int[] countBoundary(int[] arr, int bucketSize) {
int[] minAndMax = findMaxAndMin(arr);
int min = minAndMax[0], max = minAndMax[1];
int boundary = (max - min) / bucketSize;
if (boundary < 1) {
throw new IllegalArgumentException("桶数量设置不合理!");
}
return new int[]{(max - min) / bucketSize, min}; // i - min / boundary 就是元素桶的下标
}
/**
* 对每个桶进行排序
*/
private void sortEachBucket(ArrayList<Integer>[] bucket) {
for (ArrayList<Integer> singleBucket : bucket) {
Collections.sort(singleBucket);
}
}
/**
* 填充桶
*
* @param arr 数组
* @param bucket 桶
* @param bucketSize 桶大小
* @param min 元素最小值
*/
private void fillBucket(int[] arr, ArrayList<Integer>[] bucket, int bucketSize, int min, int boundary) {
for (int num : arr) {
int countedIndex = (num - min) / boundary;
int realIndex = countedIndex == bucketSize ? bucketSize - 1 : countedIndex;
bucket[realIndex].add(num);
}
}
/**
* 寻找数组中的最大值和最小值
*/
private int[] findMaxAndMin(int[] arr) {
int minIndex = 0, maxIndex = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] < arr[minIndex]) {
minIndex = i;
continue;
}
if (arr[i] > arr[maxIndex]) {
maxIndex = i;
}
}
return new int[]{arr[minIndex], arr[maxIndex]};
}
10. 基数排序
基本思想:首先计算出数组中最大值的位数 digits,对数组进行 digits 轮排序,即先按个位排序、再按十位排序...,直到按最大位排序。只适用于非负整数排序。
时间复杂度:对每一轮排序,都需要先收集后释放所有元素,因此时间复杂度。为最大元素的位数。
稳定性:稳定。
@Override
public void sort(int[] arr) {
// 计算数组最大值的位数
int digits = countDigit(arr);
// 基数桶,横坐标:0~9表示位值,每行都存储了一些元素
int[][] buckets = new int[10][arr.length];
// 标记每个基数桶中存储了多少个元素
int[] bucketCount = new int[10];
// 对数组进行 digits 轮排序
for (int i = 0, n = 1; i < digits; i++, n *= 10) {
// 收集
for (int num : arr) {
int bucketIndex = (num / n) % 10;
buckets[bucketIndex][bucketCount[bucketIndex]++] = num;
}
// 释放
int index = 0;
for (int j = 0; j < buckets.length; j++) {
for (int k = 0; k < bucketCount[j]; k++) {
arr[index++] = buckets[j][k];
}
}
// 清空 bucketCount
clearBucket(bucketCount);
}
}
private void clearBucket(int[] bucketCount) {
for (int i = 0; i < bucketCount.length; i++) {
bucketCount[i] = 0;
}
}
/**
* 计算数组中最大值的位数
* @param arr
* @return
*/
private int countDigit(int[] arr) {
// 找到最大值
int max = findMax(arr);
// 获取位数
return getDigits(max);
}
/**
* 获取 num 的位数
*/
private int getDigits(int num) {
int digits = 1;
while (num > 10) {
num /= 10;
digits++;
}
return digits;
}
/**
* 找到数组中的最大值
*/
private int findMax(int[] arr) {
int max = arr[0];
for (int num : arr) {
max = num > max ? num : max;
}
return max;
}