欢迎关注千羽的公众号

干货 | 手撕十大经典排序算法_数组程序员千羽


  • 死磕算法系列
  • 认识时间复杂度
  • 排序的定义
  • 1. 冒泡排序
  • 2. 插入排序
  • 3. 希尔排序
  • 4. 选择排序
  • 5. 快速排序
  • 6. 归并排序
  • 7. 计数排序
  • 8. 基数排序
  • 9. 桶排序
  • 10. 堆排序
  • 总结

源码点这里GitHub: https://github.com/nateshao/leetcode


干货 | 手撕十大经典排序算法_git_02

死磕算法系列


  1. 在面试中,基本上来第一面就是算法,特别是大厂,对算法的要求特别高!
  2. 对于程序员来说,算法好的人,写起代码来特别快(我们班有几位同学正是)。而且在遇到某个问题时,他想的到解决办法很多。

当然,学算法是件相对枯燥的事情,不过,当你懂得算法这种思路之后,你会发现,​算法​​真的是一个很神奇的东西,而且,算法引出的思想非常重要!!所以,千羽也会不断的学习​​死磕算法系列​​文章,和大家一起学习,一起进步。这篇文章主要​讲解十大经典排序算法​。话不多说,冲冲冲!

认识时间复杂度

常数时间的操作:一个操作如果和数据量没有关系,每次都是 固定时间内完成的操作,叫做常数操作。时间复杂度为一个算法流程中,常数操作数量的指标。常用O (读作big O)来表示。具体来说,在常数操作数量的表达式中, 只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分 如果记为f(N),那么时间复杂度为O(f(N))。

评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是常数项时间。

十大经典排序算法


算法

时间复杂度(平均)

时间复杂度(最坏)

时间复杂度(最优)

空间复杂度

稳定性

冒泡排序

O(n2)

O(n2)

O(n)

O(1)

稳定

选择排序

O(n2)

O(n2)

O(n2)

O(1)

不稳定

插入排序

O(n2)

O(n2)

O(n)

O(1)

稳定

希尔排序

O(n1.3)

O(n2)

O(n)

O(1)

不稳定

快速排序

O(nlog2n)

O(n2)

O(nlog2n)

O(nlog2n)

不稳定

归并排序

O(nlog2n)

O(nlog2n)

O(nlog2n)

O(n)

稳定

计数排序

O(n+k)

O(n+k)

O(n+k)

O(n+k)

稳定

基数排序

O(n*k)

O(n*k)

O(n*k)

O(n+k)

稳定

桶排序

O(n2)

O(n*k)

O(n)

O(n+k)

稳定

堆排序

O(nlog2n)

O(nlog2n)

O(nlog2n)

O(1)

稳定


排序的定义

对一序列对象根据某个关键字进行排序。

术语说明


  • 稳定 :如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
  • 不稳定 :如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
  • 内排序 :所有排序操作都在内存中完成;
  • 外排序 :由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
  • 时间复杂度 :一个算法执行所耗费的时间。
  • 空间复杂度 :运行完一个程序所需内存的大小。

1. 冒泡排序

时间复杂度 ,额外空间复杂度

冒泡排序(​​Bubble Sort​​)又称为泡式排序,是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。


思路:


  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。

干货 | 手撕十大经典排序算法_i++_03

代码实现:

package com.nateshao.basic_01_ten_sort;

/**
* @date Created by 邵桐杰 on 2021/10/29 16:52
* @微信公众号 程序员千羽
* @个人网站 www.nateshao.cn
* @博客 https://nateshao.gitee.io
* @GitHub https://github.com/nateshao
* @Gitee https://gitee.com/nateshao
* Description: 原文链接 https://zfhelo.gitee.io/2020/06/14/1/
*/
public class Code_01_BubbleSortTest {
public static void main(String[] args) {
int[] arr = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void bubbleSort(int[] arr) {
if (arr == null && arr.length < 2) {
return;
}
boolean isSwap;
// 外层循环控制比较的轮数
for (int i = 0; i < arr.length - 1; i++) {
isSwap = false;
// 内层循环进行相邻元素比较
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { // 前面的数比后面的数大则交换
swap(arr, j, j + 1);
isSwap = true;
}
}

if (!isSwap) {
return; // 代码优化,如果一轮循环中没有进行交换元素则说明数组已经是有序的了
}
}
}

/**
* 两数交换 不用第三个数
*
* @param arr
* @param i
* @param j
*/
public static void swap(int[] arr, int i, int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}

// public static void swap(int[] arr, int i, int j) {
// int temp = arr[i];
// arr[i] = arr[j];
// arr[j] = temp;
// }

// public static void swap(int[] arr, int a, int b) {
// arr[a] = arr[a] + arr[b];
// arr[b] = arr[a] - arr[b];
// arr[a] = arr[a] - arr[b];
// }
}

2. 插入排序

插入排序​的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

思路:


  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤2~5。

  7. 干货 | 手撕十大经典排序算法_数组_04
  8. 代码实现:
package com.nateshao.basic_01_ten_sort;

/**
* @date Created by 邵桐杰 on 2021/10/29 21:56
* @微信公众号 程序员千羽
* @个人网站 www.nateshao.cn
* @博客 https://nateshao.gitee.io
* @GitHub https://github.com/nateshao
* @Gitee https://gitee.com/nateshao
* Description:
*/
public class Code_02_InsertionSortTest {
public static void main(String[] args) {
int[] arr = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
insertionSort(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}

public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
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);
}
}
}

public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

3. 希尔排序

希尔排序是对​插入排序的改良​。

插入排序对已经基本有序的数组排序效率高,​但在移动元素时只能是相邻的两个元素交换希尔排序加了一个间隔使交换元素不限于相邻​。间隔逐渐缩小当缩小到1时数组已经基本有序。

当间隔缩小时不会破坏大间隔的排序

干货 | 手撕十大经典排序算法_数组_05

代码实现:

package com.nateshao.basic_01_ten_sort;

import java.util.Arrays;

/**
* @date Created by 邵桐杰 on 2021/10/30 10:44
* @微信公众号 程序员千羽
* @个人网站 www.nateshao.cn
* @博客 https://nateshao.gitee.io
* @GitHub https://github.com/nateshao
* @Gitee https://gitee.com/nateshao
* Description: 希尔排序
*/
public class Code_04_ShellSort {
public static void main(String[] args) {
int[] arr = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
shellSort(arr);
System.out.println(Arrays.toString(arr));
shellSort1(arr);
System.out.println(Arrays.toString(arr));
}

/**
* 希尔排序 针对有序序列在插入时采用交换法
*
* @param arr
*/
public static void shellSort(int[] arr) {
if (arr == null && arr.length < 2) {
return;
}
//增量gap,并逐步缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
//从第gap个元素,逐个对其所在组进行直接插入排序操作
for (int i = gap; i < arr.length; i++) {
int j = i;
while (j - gap >= 0 && arr[j] < arr[j - gap]) {
//插入排序采用交换法
swap(arr, j, j - gap);
j -= gap;
}
}
}
}

/**
* 希尔排序 针对有序序列在插入时采用移动法。
*
* @param arr
*/
public static void shellSort1(int[] arr) {
if (arr == null && arr.length < 2) {
return;
}
//增量gap,并逐步缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
//从第gap个元素,逐个对其所在组进行直接插入排序操作
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - gap]) {
while (j - gap >= 0 && temp < arr[j - gap]) {
//移动法
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
}
}

public static void swap(int[] arr, int a, int b) {
arr[a] = arr[a] + arr[b];
arr[b] = arr[a] - arr[b];
arr[a] = arr[a] - arr[b];

}
}

4. 选择排序

时间复杂度 ,额外空间复杂度

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

思路:


  1. 寻找出数组中最小(大)的一个数
  2. 将最小的数与当前遍历数组的首个元素交换
  3. 缩小遍历数组范围重复前两步

干货 | 手撕十大经典排序算法_i++_03

代码实现:

package com.nateshao.basic_01_ten_sort;

/**
* @date Created by 邵桐杰 on 2021/10/29 22:51
* @微信公众号 程序员千羽
* @个人网站 www.nateshao.cn
* @博客 https://nateshao.gitee.io
* @GitHub https://github.com/nateshao
* @Gitee https://gitee.com/nateshao
* Description: 选择排序
*/
public class Code_03_SelectionSortTest {
public static void main(String[] args) {
int[] arr = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
selectionSort(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}

public static void selectionSort(int[] arr) {
int min;
for (int i = 0; i < arr.length - 1; i++) {
min = i; // 默认当前排序数组的第一个元素为最小值
for (int j = i + 1; j < arr.length; j++) {
if (arr[min] > arr[j]) { // 发现一个更小的,更小最小值索引
min = j;
}
}

if (i != min) {
swap(arr, i, min);
}
}
}

public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

5. 快速排序

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

思路:


  1. 将左边第一个数做为基准值
  2. 从右边向左遍历寻找出一个小于基准数的值,将其赋值给左边的位置
  3. 从左边向右遍历寻找出一个小于基准数的值,将其赋值给右边的位置
  4. 重复2,3 直到左右索引相遇
  5. 将基准值放在相遇的这个坐标上
  6. 递归基准值左右两边的子数组

package com.nateshao.basic_01_ten_sort;

import java.util.Arrays;

/**
* @date Created by 邵桐杰 on 2021/10/30 12:09
* @微信公众号 程序员千羽
* @个人网站 www.nateshao.cn
* @博客 https://nateshao.gitee.io
* @GitHub https://github.com/nateshao
* @Gitee https://gitee.com/nateshao
* Description: 快速排序
*/
public class Code_05_QuickSort {
public static void main(String[] args) {
int[] arr = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}

/**
* @param arr 待排序列
* @param leftIndex 待排序列起始位置
* @param rightIndex 待排序列结束位置
*/
private static void quickSort(int[] arr, int leftIndex, int rightIndex) {
/**
* 如果左边大于右边,直接返回
*/
if (leftIndex >= rightIndex) {
return;
}

int left = leftIndex;
int right = rightIndex;
//待排序的第一个元素作为基准值
int key = arr[left];

//从左右两边交替扫描,直到left = right
while (left < right) {
while (right > left && arr[right] >= key) {
//从右往左扫描,找到第一个比基准值小的元素
right--;
}

//找到这种元素将arr[right]放入arr[left]中
arr[left] = arr[right];

while (left < right && arr[left] <= key) {
//从左往右扫描,找到第一个比基准值大的元素
left++;
}

//找到这种元素将arr[left]放入arr[right]中
arr[right] = arr[left];
}
//基准值归位
arr[left] = key;
//对基准值左边的元素进行递归排序
quickSort(arr, leftIndex, left - 1);
//对基准值右边的元素进行递归排序。
quickSort(arr, right + 1, rightIndex);
}
}

6. 归并排序

时间复杂度,额外空间复杂度

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用​分治法​(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并排序是一种稳定的排序方法。

思路:


  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾

举例子:

比如:A数组:[4,6,9]          B数组:[3,2,1]

先对数组A,B;数组A,B排好序 。定义一个新的数组C。比较A数组,小的放在前面。数组A排好序之后,将数组B复制到数组C

C数组:[1,2,3,4,6,9 ]

干货 | 手撕十大经典排序算法_i++_07

代码实现:

package com.nateshao.basic_01_ten_sort;

import java.util.Arrays;

/**
* @date Created by 邵桐杰 on 2021/10/30 12:15
* @微信公众号 程序员千羽
* @个人网站 www.nateshao.cn
* @博客 https://nateshao.gitee.io
* @GitHub https://github.com/nateshao
* @Gitee https://gitee.com/nateshao
* Description:
*/
public class Code_06_MergeSort {
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 20;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
mergeSort(arr1);
comparator(arr2);
if (!isEqual(arr1, arr2)) {
succeed = false;
printArray(arr1);
printArray(arr2);
break;
}
}
System.out.println(succeed ? "Nice!" : "Fucking fucked!");

int[] arr = generateRandomArray(maxSize, maxValue);
printArray(arr);
mergeSort(arr);
printArray(arr);

}
/**
* 如果数组的长度为空,或者是数组的长度为1。直接返回,不需要比较
* @param arr
*/
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
mergeSort(arr, 0, arr.length - 1);
}

/**
* 这个范围上只有一个数,直接返回
* @param arr
* @param l
* @param r
*/
public static void mergeSort(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = l + ((r - l) >> 1); // L和R中点的位置 (L + R)/ 2
mergeSort(arr, l, mid); // 左部分排序 T(n/2)
mergeSort(arr, mid + 1, r); // 右部分排序 T(n/2)
merge(arr, l, mid, r); // 左部分和右部分合并 O(n)
// 总的时间复杂度:T(N) = 2T(n/2) + O(N)
}

public static void merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;// 数组l,左侧第一的最小值。
int p2 = m + 1;// 右侧第一的最小值。
while (p1 <= m && p2 <= r) { // p1 or p2 谁小取谁,放在新的数组,重新排序数组
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
/**
* p1,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]; // 最后将数组拷贝到arr[i]
}
}

/**
* 比较器
* @param arr
*/
public static void comparator(int[] arr) {
Arrays.sort(arr);
}

/**
* 生成任意数组
* @param maxSize
* @param maxValue
* @return
*/
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}

/**
* 复制数组
* @param arr
* @return
*/
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}

/**
* 两数组是否相等
* @param arr1
* @param arr2
* @return
*/
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}

/**
* 打印数组
* @param arr
*/
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}

7. 计数排序

计数排序是桶排序的一种。是一个非基于比较的排序算法。

它的优势在于在对一定范围内的整数排序时,它的复杂度为(其中k是整数的范围),快于任何比较排序算法。​当然这是一种牺牲空间换取时间的做法​,而且当 的时候其效率反而不如基于比较的排序

思路:


  1. 找出数组中最大最小值
  2. 创建对应长度的数组(max - min +1)
  3. 统计每个数出现的次数 array[val - min]++
  4. 填充目标数组

干货 | 手撕十大经典排序算法_i++_08

代码实现:

package com.nateshao.basic_01_ten_sort;

import java.util.Arrays;

/**
* @date Created by 邵桐杰 on 2021/10/30 14:38
* @微信公众号 程序员千羽
* @个人网站 www.nateshao.cn
* @博客 https://nateshao.gitee.io
* @GitHub https://github.com/nateshao
* @Gitee https://gitee.com/nateshao
* Description: 计数排序
* 原文链接:https://baijiahao.baidu.com/s?id=1613842852280800384&wfr=spider&for=pc
*/
public class Code_07_CountSort {

public static void main(String[] args) {
int[] array = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
int[] sortedArray = countSort(array);
System.out.println(Arrays.toString(sortedArray));
}

public static int[] countSort(int[] array) {
//1.得到数列的最大值和最小值,并算出差值
int max = array[0];
int min = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
if (array[i] < min) {
min = array[i];
}
}
int d = max - min;
//2.创建统计数组并统计对应元素个数
int[] countArray = new int[d + 1];
for (int i = 0; i < array.length; i++) {
countArray[array[i] - min]++;
}
//3.统计数组做变形,后面的元素等于前面的元素之和
int sum = 0;
for (int i = 0; i < countArray.length; i++) {
sum += countArray[i];
countArray[i] = sum;
}
//4.倒序遍历原始数列,从统计数组找到正确位置,输出到结果数组
int[] sortedArray = new int[array.length];
for (int i = array.length - 1; i >= 0; i--) {
sortedArray[countArray[array[i] - min] - 1] = array[i];
countArray[array[i] - min]--;
}
return sortedArray;
}
}

计数排序的局限性:

1.当数列最大最小值差距过大时,并不适用计数排序。

比如给定20个随机整数,范围在0到1亿之间,这时候如果使用计数排序,需要创建长度1亿的数组。不但严重浪费空间,而且时间复杂度也随之升高。

2.当数列元素不是整数,并不适用计数排序。

如果数列中的元素都是小数,比如25.213,或是0.00000001这样子,则无法创建对应的统计数组。这样显然无法进行计数排序。

8. 基数排序

基数排序是桶排序的一种。原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。每位比较完成后将数组重新排序。当最高位完成比较时,数组也就变成了一个有序序列。

干货 | 手撕十大经典排序算法_数组_09

代码实现:

package com.nateshao.basic_01_ten_sort;

import java.util.Arrays;

/**
* @date Created by 邵桐杰 on 2021/10/30 15:15
* @微信公众号 程序员千羽
* @个人网站 www.nateshao.cn
* @博客 https://nateshao.gitee.io
* @GitHub https://github.com/nateshao
* @Gitee https://gitee.com/nateshao
*/
public class Code_08_RadixSort {
public static void main(String[] args) {
int[] array = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
radixSort(array);
System.out.println(Arrays.toString(array));
}

public static void radixSort(int[] arr) {
// 存数组中最大的数字,为了知道循环几次
int max = Integer.MIN_VALUE;// (整数中的最小数)
// 遍历数组,找出最大值
for (int i = 0; i < arr.length; i++) {
if (max < arr[i]) {
max = arr[i];
}
}

// 计算最大数是几位数,,此方法计较绝妙
int maxLength = (max + "").length();
// 用于临时存储数据的数组
int[][] temp = new int[10][arr.length];
// 用于存储桶内的元素位置
int[] counts = new int[arr.length];

// 第一轮个位数较易得到余数,第二轮就得先除以十再去取余,之后百位除以一百
// 可以看出,还有一个变量随循环次数变化,为了取余

// 循环的次数
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
// 每一轮取余
for (int j = 0; j < arr.length; j++) {
// 计算余数
int ys = (arr[j] / n) % 10;
// 把便利店数据放在指定数组中,有两个信息,放在第几个桶+数据应该放在第几位
temp[ys][counts[ys]] = arr[j];
// 记录数量
counts[ys]++;
}

// 记录取的数字应该放到位置
int index = 0;
// 每一轮循环之后把数字取出来
for (int k = 0; k < counts.length; k++) {
// 记录数量的数组中当前余数记录不为零
if (counts[k] != 0) {
for (int l = 0; l < counts[k]; l++) {
// 取出元素
arr[index] = temp[k][l];
index++;
}
// 取出后把数量置为零
counts[k] = 0;
}
}
}
}
}

9. 桶排序

桶排序(Bucket sort)​ 或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序。

思路:


  1. 设置一个定量的数组当作空桶子。
  2. 寻访序列,并且把项目一个一个放到对应的桶子去。
  3. 对每个不是空的桶子进行排序。
  4. 从不是空的桶子里把项目再放回原来的序列中。

干货 | 手撕十大经典排序算法_i++_10

代码实现:

package com.nateshao.basic_01_ten_sort;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

/**
* @date Created by 邵桐杰 on 2021/10/30 15:56
* @微信公众号 程序员千羽
* @个人网站 www.nateshao.cn
* @博客 https://nateshao.gitee.io
* @GitHub https://github.com/nateshao
* @Gitee https://gitee.com/nateshao
* Description: 桶排序
*/
public class Code_09_BucketSort {

public static void main(String[] args) {
int[] arr = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
bucketSort(arr);
System.out.println(Arrays.toString(arr));
}

public static void bucketSort(int[] arr) {
int max = arr[0];
int min = arr[0];
for (int i = 1; i < arr.length; i++) {
if (max < arr[i]) {
max = arr[i];
}
if (min > arr[i]) {
min = arr[i];
}
}
int bucketNum = (max - min) / 10 + 1; // 桶的数量
List[] buckets = new ArrayList[bucketNum];
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new ArrayList<Integer>();
}

// 把元素装入桶中
for (int i = 0; i < arr.length; i++) {
int a = (arr[i] - min) / 10;
buckets[a].add(arr[i]);
}

int index = 0;
for (List<Integer> b : buckets) {
insertionSort(b); // 排序桶内元素
for (Integer num : b) {
arr[index++] = num; // 取出排序好的元素
}
}
}

// 插入排序
public static void insertionSort(List<Integer> arr) {
for (int i = 1; i < arr.size(); i++) {
// 从第二个开始查看是否比前一个小
if (arr.get(i) < arr.get(i - 1)) {
// 逐个往前找如果大于此数则将其往后移动一位
int temp = arr.get(i);
int j = i - 1;
while (j >= 0 && temp < arr.get(j)) {
arr.set(j + 1, arr.get(j));
j--;
}
// 找到一个比它小的数----将其赋值给此数后面的元素
arr.set(j + 1, temp);
}
}
}
}

10. 堆排序

堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

堆是一个​完全二叉树​,完全二叉树就是节点从上往下从左往右中间不会有缺失的。简单点说就是一个节点如果存在右子树那么左子树也一定存在


干货 | 手撕十大经典排序算法_数组_11

堆排序描述:

干货 | 手撕十大经典排序算法_数组_12

上图中在3这个节点有右子树却没有左子树,所以这不是一个完全二叉树。

堆还分为大顶堆和小顶堆



  • 大顶堆​ 父节点值大于或等于子节点
  • 小顶堆​ 父节点值小于或等于子节点

干货 | 手撕十大经典排序算法_git_13

如果我们把一个数组上的元素都映射到一个堆上可以发现他们的index有着如下的关系左节点index = 父节点 * 2 + 1 右节点index = 父节点 * 2 + 2

干货 | 手撕十大经典排序算法_数组_14

对于一个大顶堆,根节点就是它的最大值。我们只需要根据上面的关系进行交换父子节点的元素,就可以将一个随机的数组转化为堆。然后将根节点和最后的节点交换位置。然后把缩小数组长度。在对这个数组进行堆的转化,同时又可以获取到该数组最大的数,在移动到数组最后面。在进行堆的转化…

思路:


  1. 将整个数组构建成堆
  2. 交换根节点和最后一个节点
  3. 从根节点开始heapify
  4. 重复2,3直到遍历所有元素

干货 | 手撕十大经典排序算法_git_15

代码实现:

package com.nateshao.basic_class_01;

import java.util.Arrays;

public class Code_03_HeapSort {

public static void main(String[] args) {
int testTime = 500000;
int maxSize = 20;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
heapSort(arr1);
comparator(arr2);
if (!isEqual(arr1, arr2)) {
succeed = false;
break;
}
}
System.out.println(succeed ? "Nice!" : "Fucking fucked!");

int[] arr = generateRandomArray(maxSize, maxValue);
printArray(arr);
heapSort(arr);
printArray(arr);
}

/**
* 堆排序
* @param arr
*/
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 建立大根堆
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int size = arr.length;
swap(arr, 0, --size); // 最后一位数,与第一位数交换。 堆大小减1
while (size > 0) {
heapify(arr, 0, size); // 继续调整大根堆
swap(arr, 0, --size); // 继续。最后一位数,与第一位数交换。 堆大小减1
}
}

/**
* 建立大根堆
*
* @param arr
* @param index
*/
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}

public static void heapify(int[] arr, int index, int size) {
int left = index * 2 + 1; // 左孩子。i*2+1 右孩子:left + 1
while (left < size) { // 堆大小
// 左孩子有孩子比较,谁的值大就取谁
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
// 节点与左右孩子比较,谁大取谁下标
largest = arr[largest] > arr[index] ? largest : index;
// 是我自己,就不用往下执行
if (largest == index) {
break;
}
swap(arr, largest, index); // largest != index
index = largest;
left = index * 2 + 1;
}
}

public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}

public static void comparator(int[] arr) {
Arrays.sort(arr);
}

public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}

public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}

public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}

public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}

总结

以上就是经典的十大排序算法了。实际上,你去面试某公司,上来啥都不问,就考你算法。但是在工作上,算法几乎用不到。

但是没办法,现实就是这样。当然​算法是作为一名合格程序员所要掌握的最基本的知识​,一线开发岗位,包括架构师和 Leader,只要还在写代码,那么面试中必然是要包括算法这部分内容的。

参考链接:

  1. 十大经典排序算法:https://zfhelo.gitee.io/2020/06/14/1
  2. 计数排序:https://baijiahao.baidu.com/s?id=1613842852280800384&wfr=spider&for=p



有问题可以联系千羽哦

干货 | 手撕十大经典排序算法_git_16