选择排序
每趟从未排序的序列中选出最小值,放到已排序序列的末尾。
private void selectSort(int[] arr) {
int len = arr.length;
for (int i = 0; i < len - 1; i++) {
// 每一趟从未排序的序列中选出最小值的下标ail
int minIndex = i;
for (int j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 选出的最小值成为已排序序列的末尾,这通过一次交换实现
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
// tip:第一趟i指向0,然后从1开始找最小值,交换二者
插入排序
对于当前的数字,在已排序的序列中从后向前扫描,找到正确的位置并插入。
private void insertionSort(int[] arr) {
int len = arr.length;
for (int i = 1; i < len; i++) {
int temp = arr[i]; // 暂存arr[i]的值,因为它会因为后挪而被覆盖
int index = i; // index指向arr[i]应该存放的位置
while (index - 1 >= 0 && arr[index - 1] > temp) {
arr[index] = arr[index - 1];
index--;
}
arr[index] = temp;
}
}
// tip:第一个元素不用插入到前面,所以遍历从1开始
冒泡排序
相邻的元素进行两两比较,顺序相反则进行交换,这样,每一趟会将最大元素“冒泡”到这趟的最后。
private void bubbleSort(int[] arr) {
int len = arr.length;
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < len - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// tip:for(i < len - 1)和for(j < len - i - 1)要背熟
希尔排序
将序列依据增量分组,对每组使用直接插入排序算法;当增量二分减小到1时,对序列进行最后一次排序。
private void shellSort(int[] arr) {
int len = arr.length;
for (int gap = len / 2; gap > 0; gap /= 2) { // 增量二分减小
for (int i = gap; i < len; i++) { // 多组插入排序交叉进行(这里需要理解)
int temp = arr[i];
int index = i;
while (index - gap >= 0 && arr[index - gap] > temp) {
arr[index] = arr[index - gap];
index -= gap;
}
arr[index] = temp;
}
}
}
// tip:gap是不断二分的;内层的for循环是for(int i = gap; i < len; i++)
快速排序
从序列中选出一个基准(最左侧元素),走一躺之后,基准被换到了中间,基准左侧的元素都比基准要小,基准右侧的元素都比基准要大,然后递归排序左侧和右侧的序列。
private void quickSort(int[] arr, int L, int R) {
if (L >= R) {
return;
}
int left = L;
int right = R;
// 基准
int temp = arr[left];
while (left < right) {
// 找到右侧第一个比基准小的元素
while (left < right && arr[right] >= temp) {
right--;
}
arr[left] = arr[right];
// 找到左侧第一个比基准大的元素
while (left < right && arr[left] <= temp) {
left++;
}
arr[right] = arr[left];
}
arr[left] = temp;
// 最后left和right会收缩到基准点的正确位置处
quickSort(arr, L, left - 1);
quickSort(arr, left + 1, R);
}
// arr[a]=arr[b],则arr[b]处冗余,等待被覆盖
归并排序
使用分治的思想,先排左子序列,再排右子序列,然后二路归并;对于左子序列/右子序列,分别递归。
private void mergeSort(int[] arr, int left, int right) {
if (left >= right) {
return;
}
int mid = left + (right - left) / 2; // 二分为左右子序列
mergeSort(arr, left, mid); // 左子序列排序
mergeSort(arr, mid + 1, right); // 右子序列排序
merge(arr, left, mid, right); // 二路归并
}
private void merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[arr.length]; // 辅助数组,可提升作用域防止重复开辟
int i = left;
int j = mid + 1;
int t = left;
while (i <= mid && j <= right) {
if (arr[i] < arr[j]) {
temp[t++] = arr[i++];
} else {
temp[t++] = arr[j++];
}
}
while (i <= mid) {
temp[t++] = arr[i++];
}
while (j <= right) {
temp[t++] = arr[j++];
}
while (left <= right) {
arr[left] = temp[left];
left++;
}
}
// tip:二路归并merge方法需要三个指针参数;注意mid指向的位置时偏左的
堆排序
先通过多次调整堆构建出大顶堆,然后不断进行交换堆顶和堆尾元素、调整大顶堆的操作。
private void heapSort(int[] arr) {
int len = arr.length;
// 构建大顶堆,即从第一个非叶子结点从下至上、从右至左调整堆结构
for (int i = len / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, len);
}
// 交换堆顶和堆尾元素+调整大顶堆
for (int j = len - 1; j > 0; j--) {
int temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustHeap(arr, 0, j);
}
}
// 调整大顶堆
private void adjustHeap(int[] arr, int i, int len) {
int temp = arr[i];
for (int k = i * 2 + 1; k < len; k = k * 2 + 1) {
if (k + 1 < len && arr[k] < arr[k + 1]) { // 保证k指向i的两个子结点的最大者
k++;
}
if (arr[k] > temp) { // 本次调整可能破坏下面的子堆,所以将i置为k继续调整
arr[i] = arr[k];
i = k;
} else {
break;
}
}
arr[i] = temp;
}
// tip:13526的建堆过程极其经典
计数排序
计数排序不是基于比较的,而是将元素存储到额外的存储空间中,然后取出。
private void countingSort(int[] arr, int min, int max) {
int[] count = new int[max - min + 1];
for (int n : arr) {
count[n - min]++;
}
int index = 0;
for (int i = 0; i < count.length; i++) {
while (count[i] > 0) {
arr[index++] = min + i;
count[i]--;
}
}
}
// tip:注意偏移
桶排序
将元素放到有限数量的桶里,在每个桶再分别排序。
private void bucketSort(int[] arr, int min, int max) {
int len = arr.length; // 桶的容量
int bucketNum = (max - min) / len + 1; // 桶的数量
// 初始化桶
List<List<Integer>> buckets = new ArrayList<>();
for (int i = 0; i < bucketNum; i++) {
buckets.add(new ArrayList<>());
}
// 元素放入桶
for (int n : arr) {
int id = (n - min) / len;
buckets.get(id).add(n);
}
// 桶内排序
for (List<Integer> bucket : buckets) {
Collections.sort(bucket);
}
int index = 0;
for (List<Integer> bucket : buckets) {
for (int n : bucket) {
arr[index++] = n;
}
}
}
// tip:可能一个桶要装所有的元素,所以桶的容量为arr.length;为了保证最大值由桶可装,桶的数量向上+1
基数排序
先根据个位进行桶排序,再根据十位进行桶排序,再根据百位进行桶排序…整个序列逐渐完成排序。
private void radixSort(int[] arr, int max) {
int len = arr.length;
int[][] buckets = new int[10][len]; // 桶
int[] bucketSize = new int[10]; // 每个桶的实际容量
for (int i = 0, mod = 1; i < String.valueOf(max).length(); i++, mod *= 10) {
// 元素入桶
for (int n : arr) {
int id = n / mod % 10;
buckets[id][bucketSize[id]++] = n;
}
// 元素出桶
int index = 0;
for (int j = 0; j < 10; j++) {
for (int k = 0; k < bucketSize[j]; k++) {
arr[index++] = buckets[j][k];
}
bucketSize[j] = 0;
}
}
}
// tip:基数排序是个逐渐有序的过程
复杂度与稳定性
时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 | |
---|---|---|---|---|---|
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
插入排序 | O(n²) | O(n²) | O(n) | O(1) | 稳定 |
冒泡排序 | O(n²) | O(n²) | O(n) | O(1) | 稳定 |
希尔排序 | O(n)~O(n²) | O(1) | 不稳定 | ||
快速排序 | O(nlog2n) | O(n²) | O(nlog2n) | O(log2n) | 不稳定 |
归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 稳定 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
桶排序 | O(n+k) | O(n²) | O(n) | O(k) | 稳定 |
基数排序 | O(n*r) | O(n*r) | O(n*r) | O(k) | 稳定 |
☘️