进大厂必会的几个常用排序算法的图解以及详细代码
排序算法算是我们接触过的最普及的算法,正是因为如此,诞生了很多的排序的算法,像我们熟知的冒泡排序、选择排序、插入排序,这几个的原理非常简单,弄懂之后代码实现也比较简单,这里作者就不再重复老套了,不知道的同学可以搜索相关博客和视频学习。
接下来本篇主要介绍的就是五个相对复杂一点的排序算法,分别是快速排序,希尔排序,归并排序,基数排序以及堆排序,可能有的同学会问已经有上面几个排序算法了,为什么还会诞生这么多不同的算法,其实这个问题认真思考就能知道了,最简单一点就是上面几个算法在一定程度不能满足我们一些需求的实现了,这就要说到一段算法或者代码的时间复杂度了,可以简单理解的就是一段代码内有效代码的执行的次数,想要搞懂明确含义的可以查找相关资料进行学习。
回到主题,本篇主要还是介绍这五个排序算法的图解以及代码实现的,这是作者呕心沥血花费一整天时间整理出来的,作者也是一个算法小白,大家可以多多交流讨论分享,有什么不对的地方希望大佬指点。
一 快速排序
快速排序的思想就是通过不断转换轴点元素最终完成排序
什么是轴点元素?
按升序排序来说,轴点元素就是左边所有元素比该元素小,右边所有元素比该元素大
以下面的图为例子,我们首先需要假设一个轴点元素,然后将所有比它小的放在它左边,比它大的放在它右边,然后通过递归的方式完成最终的排序
过程如下:
1)首先要先定义两个指针,开始时分别指向首尾,分别为left,right
2)假设以第一个元素为轴点元素,我们首先从又右开始往左扫描,用right位置的值与假设索引值进行比较,
①如果比pivot元素值大就不动,并且right指针向左移动一次,重复上述操作
②如果比pivot元素值小就将值赋给left所对应的位置,left向右移动一次,此时需要反向,从左往右开始扫描,重复上述操作
3)重复上述2的操作直到临界点,即left < right,退出操作
4)将假设轴点元素的值赋给最后的索引所在的位置,并返回索引
此时第一轮的选取轴点元素就完成了,且我们可以知道轴点元素左边都是比轴点元素小的,我们可以从left到轴点元素为界限再选取一次轴点元素,以轴点元素到right为界限选取一次轴点元素,依次类推,我们可以很轻松的就将所有元素排好序了,递归时我们需要确定明确的界限保证递归函数能够回溯,不然会造成死循环的情况,我们可以知道此时的界限就是需要left < right,在这个界限内可以一直调用函数直到满足界限,即只剩下一个元素,也就保证了所有元素的排序成功了
代码如下:
public class QuickSort {
public static void main(String[] args) {
int[] arr = {6,3,5,8,1,9,7,2,0};
int i = pivotIndex(arr, 0, arr.length);
System.out.println(Arrays.toString(arr));
}
//对[begin,end) 范围内的元素进行快速排序
public static void quickSort(int[] arr,int begin,int end){
if (end - begin < 2){
return;
}
//对范围内元素进行快速排序确定轴点
int mid = pivotIndex(arr, begin, end);
//对轴点元素左边进行快速排序
quickSort(arr, begin, mid);
//对轴点元素右边进行快速排序
quickSort(arr, mid + 1, end);
}
/**
* 构建范围在[begin,end)范围内元素的轴点元素
*
* @return 返回轴点元素的最终位置
*/
public static int pivotIndex(int[] arr,int begin,int end){
//备份begin元素值
int pivot = arr[begin];
//将end元素指向最后一个元素
end--;
//具体操作逻辑构建轴点元素位置
while (begin < end){
while(begin < end){
//从右往左判断
if (pivot < arr[end]){
end--;
}else {
arr[begin++] = arr[end];
break;
}
}
while(begin < end){
//从左往右判断
if (pivot > arr[begin]){
begin++;
}else {
arr[end--] = arr[begin];
break;
}
}
}
//将备份的轴点元素放入最终的位置
arr[begin] = pivot;
return begin;
}
}
二 希尔排序
希尔排序就是直接插入排序的优化,因为考虑到插入排序的极限情况下可能造成最坏的时间复杂度
其原理就是基于插入排序的基础上,先是将整个数组按照step(元素个数/2)分组,相同组之间元素相隔就是step值,如下图所示
相同组内的元素进行比较,按升序排序来说,将小的放在前面,大的放在后面
第二次将step /= 2,再次进行上述操作,直到step = 1 此时就是正常的插入排序了,但是我们可以看出经过上述操作之后,数组已将近排序完成
希尔排序与插入排序最大的不同就是要考虑一个分组的情况,其他与插入排序的思想完全一样
代码如下:
下面有两种实现方式和直接插入排序一样,交换式和移位式
//希尔排序,就是进行分组之后的直接插入排序
public class ShellSort {
public static int count = 0;
public static void main(String[] args) {
int[] arr = {6,3,5,8,1,9,7,2,0};
shellsort1(arr);
}
//希尔排序之交换式
public static void shellsort(int[] arr){
for (int step = arr.length/2; step > 0 ; step /= 2){
for (int i = step; i < arr.length ; i++) {
for (int j = i-step; j >= 0 ; j -= step) {
if (arr[j] > arr[j+step]){
int temp = arr[j];
arr[j] = arr[j+step];
arr[j+step] = temp;
}
}
}
System.out.println("希尔排序第"+ ++count +"轮:" + Arrays.toString(arr));
}
}
//希尔排序之移位式
public static void shellsort1(int[] arr){
//将数组分组,并进行判断移位
for (int step = arr.length/2; step > 0 ; step /= 2) {
//这个就和简单的插入排序基本相同,只是多了一个分组的步长,需要考虑每一轮分组的情况
for (int i = step; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - step]){
while (j - step >= 0 && temp < arr[j - step]){
arr[j] = arr[j - step];
j -= step;
}
}
arr[j] = temp;
}
}
System.out.println(Arrays.toString(arr));
}
}
三 归并排序
归并排序是利用分治思想实现的一种排序算法
分:利用递归来实现将数组分解开成为最小的单位,即一个个元素
并:在递归函数回溯的过程中,对分解开的元素进行排序,回溯完成排序也就完成了
具体如下图所示:
接下来就是在回溯过程中具体实现排序的过程以及图解
整个回溯过程中的排序有以下几步:
1)首先需要一个临时数组,用来存放排序好的元素
2)需要三个指针left、mid、right,用来定位元素的具体位置,并进行操作
3)开始排序思路
①因为都是顺序排序,都是从左往右进行操作,比较left与tmp所在索引位置的值,小的就往临时数组放,此时索指针向右移动一次,数组索引从0开始
②重复上述操作,直到达到临界条件,即left <= mid && tmp <= right
③将剩下的元素依次放入临时数组,此时数组就是完全有序的了
④此时要将临时数组的值依次拷贝到原数组的对应索引的位置
(注意:很重要,因为是递归回溯的过程,每次的指针对应的就是分解的那部分的首尾和中间位置,所以上一次排好的顺序需要在这一次的排序中使用,所以要将排好序的临时数组拷贝到原数组所对应索引的位置,在接下来的代码会体现出来)
代码如下:
//归并排序
public class MergeSort {
public static void main(String[] args) {
int[] arr = {6,3,5,8,1,9,7,2,0};
int[] temp = new int[arr.length];
mergeSort(arr,0,arr.length-1,temp);
System.out.println(Arrays.toString(arr));
}
//分解并且合并
public static void mergeSort(int[] arr,int left,int right,int[] temp){
if (left < right){
int mid = (left+right)/2;
mergeSort(arr,left,mid,temp);
mergeSort(arr,mid+1,right,temp);
merge(arr,left,mid,right,temp);
}
}
/**
* 合并
* @param arr 原来的数组
* @param left 拆分的左边序列的初始索引
* @param mid 拆分的左边序列的结束索引
* @param right 拆分的右边序列的结束索引
* @param temp 用来做中转的数组
*/
public static void merge(int[] arr ,int left,int mid,int right,int[] temp){
int i = left;
int j = mid + 1;
int t = 0;
//将序列的数组中数据按顺序放到临时数组中
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++];
}
//copy临时数组数据到原来数组相同的位置
t = 0;
int tempLeft = left;
System.out.println("tempLeft="+tempLeft+",right="+right);
while (tempLeft <= right){
arr[tempLeft++] = temp[t++];
}
}
}
四 基数排序
利用一个二维数组来保存排序的元素,是将一个数字拆分为一位位的数来进行比较大小排序
二维数组的大小是十,对应的索引正好是每位数所对应的值,需要先在数组中找出最大值,然后判断最大值位数来决定进行多少次的比较和排序
此时还需要一个一维数组来确定二维数组中每个一维数组中的有效数据,方便从二维数组取出数据到原数组
其原理就是一位位数的比较大小并排序,思想很简单,但是代码实现还是需要好好思考一下的
代码如下:
//基数排序(桶排序的优化)
public class RadixSort {
public static void main(String[] args) {
int[] arr = {542,3,652,784,69,123,15};
radixSort(arr);
}
public static void radixSort(int[] arr){
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max){
max = arr[i];
}
}
int maxLenth = String.valueOf(max).length();
int[][] bucket = new int[10][arr.length];
//每个桶中记录的有效元素个数
int[] bucketEleCounts = new int[10];
for (int k = 0,n = 1; k < maxLenth; k++,n *= 10) {
//将原来数组元素放到桶中
for (int i = 0; i < arr.length; i++) {
int num = arr[i] / n % 10;
bucket[num][bucketEleCounts[num]++] = arr[i];
}
int index = 0;//记录原来数组的索引值
//将桶中数据再依次放到原来数组中
for (int i = 0; i < bucket.length; i++) {
if (bucketEleCounts[i] != 0){
for (int j = 0; j < bucketEleCounts[i]; j++) {
arr[index++] = bucket[i][j];
}
}
bucketEleCounts[i] = 0;
}
System.out.println("第"+k+"次排序后的数组:"+ Arrays.toString(arr));
}
}
}
五 堆排序
堆排序放在最后面写是因为是这几种中唯一一个需要用到树这个数据结构的思想的算法,对于一些没有接触过树的朋友来说可能理解起来比较困难
我们需要知道堆排序需要用到什么?
堆排序是将一个顺序存储的二叉树转换为堆结构,一般升序用大顶堆,即叶子结点的父节点比叶子结点都大;降序用小顶堆,即叶子结点的父节点比叶子结点都小
那又要问了什么是顺序存储的二叉树,我们需要知道的是,二叉树和数组这两种数据结构是可以互相转换的
顺序存储二叉树
用数组的形式存放二叉树结构数据
1)顺序存储二叉树通常只考虑完全二叉树
完全二叉树:
有k层的树结构,其中除了第k层之外都是满节点,且尽量保证第k层左子树为满节点
2)第n个元素的左子节点为2*n+1
3)第n个元素的右子节点为2*n+2
4)第n个元素的父节点为(n-1)/ 2
可能说了这么多有些朋友还是不是太懂,接下里我用图示的方式展现一下
具体的转换过程如下:
二叉树转换为大顶堆的具体步骤:
①从最末尾元素的父节点开始与左右子节点进行比较,将其中最大的元素与父节点进行交换
②然后以目前结点为根节点判断左右子结点的子树是否满足大顶堆的条件,依次往下判断,确保以目前结点为根节点下的所有子树都满足大顶堆的条件
一轮结束后将根节点与最末尾元素进行交换,并将长度减一,即排好的最大元素排除,之后的操作不会考虑这个元素
再次重复上述转换大顶堆的过程,直到元素全部排好序
大家可以对照图示跟着代码走一遍,应该会理解的更深一点
接下来是代码:
public class HeapSort {
public static void main(String[] args) {
int[] arr = {4,6,8,5,9,99,13,-6,-66,98,123};
heapSort(arr);
}
public static void heapSort(int[] arr){
int temp = arr[0];
//找出最大的值放在堆的根节点
for (int i = arr.length/2 -1; i >= 0 ; i--) {
adjustHeap(arr,i,arr.length);
}
//交换根节点与最后一个节点的位置并移除最后一个节点
for (int i = arr.length-1; i > 0 ; i--) {
temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
adjustHeap(arr,0,i);
}
System.out.println(Arrays.toString(arr));
}
/**
* 将数组转换为大顶堆
*
* @param arr 要转换的数组
* @param i 非叶子结点在数组中的索引
* @param length 表示对多少个元素进行调整
*/
public static void adjustHeap(int[] arr,int i,int length){
int temp = arr[i];
for (int j = 2 * i + 1; j < length; j = 2 * j + 1) {
if (j+1 < length && arr[j] < arr[j+1]){
j++;
}
if (arr[j] > temp){
arr[i] = arr[j];
i = j;
}else {
break;
}
}
arr[i] = temp;
}
}
说实话有点难理解,我自己也思考了半天,可能是我的水平还不够,不能通俗易懂的讲出来,希望有大佬可以帮忙指出有什么问题或者是什么可以改进的地方。
大家看完了如果觉得对自己有一点帮助可以点赞一波,这是第一次写一篇上万字的文章,还希望如果有什么问题希望大家谅解。