这里我会介绍冒泡、选择、插入、希尔、归并、快速、基数排序算法,而堆排序在后续的更新中会介绍这种排序算法(介绍完树后)
堆排序已发,在此附上链接。堆排序思路及其实现
稳定性的概念为:假如要排序的数列出现了相同的数据,则原本在左边的数据仍然在左边,右边的数据仍然在右边,即不会改变这两个数据的顺序,这种情况称为稳定。
我们这里都实现从小到大排序
目录
冒泡排序
冒泡排序是一种通过不断交换相邻值,最终实现排序的一种排序算法。
其基本思路为遍历一组数列中,如果当前遍历的数据比当前数据的后一个数据大,就将两个数据进行交换。如此操作就在第一轮大循环中,把最大的数据放到了数列中的最后面,然后进行第二次循环,第二次循环又把倒数第二大的数字放到了数列中的倒数第二个位置….如此反复,最终就可以得到一个有序的数列。
每一轮大循环结束,则进入下一轮循环
接下来继续进行第三轮、第四轮.....最终能够获得一个有序数组
因此也就实现了冒泡排序
下面附上代码实现,在代码中我进行了些许优化,大家可以在代码中探究
package com.liu.sortalgorithm;
import java.util.Arrays;
/**
* @author liuweixin
* @create 2021-09-09 19:24
*/
//冒泡排序算法
public class BubbleSortAlgorithm {
public static void main(String[] args) {
int[] arr = new int[]{-1, 3, 5, 11414, 112, 426, 112, -2};
System.out.println("排序前:" + Arrays.toString(arr));
System.out.println();
BubbleSortAlgorithm.sort(arr);
System.out.println("排序后:" + Arrays.toString(arr));
}
/**
* 进行冒泡排序操作
*
* @param arr 传入要排序的数组
*/
public static void sort(int[] arr) {
int temp;//借助一个临时变量实现交换
boolean flag;//提高效率,具体如何实现看下面的代码
for (int i = 0; i < arr.length-1; i++) {//大循环为数组的长度-1
flag=false;//重置flag的值
for (int j = 0;j<arr.length-1-i;j++) {//减i的原因是可以提高效率,因为最大的数已经排到了数组的最后面,此时无需再进行比较
//此时只需要到达倒数第二位,否则会在下面的与最后一个数比较时越界
if (arr[j] > arr[j+1]) {
//如果前一个数字大于后一个数字,则需要两数交换
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
//如果进行了交换,更改flag
flag=true;
}
}
if(!flag){
//如果此时flag的值是flag,则说明本次循环中没有进行交换,那就说明此时已经提前排序完毕,
//此时就没有必要再继续向后继续大循环
break;
}
}
}
}
选择排序
选择排序的本质有点类似与冒泡排序,先找出数列中最小的值,然后把该值放至第一位,然后再遍历剩下的数据,再找出最小的值,放到第二位,如此反复,最后可以得到一个有序数列。
与冒泡排序类似的是(都以从小到大的举例):冒泡排序是把最大的数放置到最后,而选择排序则是将最小的数放置在最前面。而区别是:
- 冒泡排序属于稳定排序,而选择排序则是不稳定排序。
- 二者的交换成本不一样,冒泡排序是发现两者逆序,则马上交换;而选择排序是一直找,遍历完所有数据后找到最小的数才进行交换,故选择排序的交换成本较小。
每次大循环,然后接跟着有小循环,与冒泡排序同理,大循环只需数组长度-1次即可
下面附上代码实现:
package com.liu.sortalgorithm;
import java.util.Arrays;
/**
* @author liuweixin
* @create 2021-09-09 19:47
*/
//选择排序
public class SelectSortAlgorithm {
public static void main(String[] args) {
int[] arr = new int[20];
for (int i = 0; i < arr.length; i++) {
arr[i]= (int) (Math.random()*800000);
}
System.out.println("排序前:" + Arrays.toString(arr));
SelectSortAlgorithm.sort(arr);
System.out.println("排序后:" + Arrays.toString(arr));
}
/**
* 进行选择排序
*
* @param arr 传入要排序的数组
*/
public static void sort(int[] arr) {
int num;//记录最小值
int index;//记录最小值的下标
int temp;//借助辅助变量进行交换
for (int j = 0; j < arr.length-1; j++) {//大循环,只需循环数组长度-1次
num=arr[j];//先把arr数组的未排序的第一个数字当作最小值,而且也可以做到重置num的值
index=j;//同理index也需要初始化和重置
for (int i = j; i < arr.length; i++) {//小循环,找寻最小值,i的起始值为j,因为最小值已经放置前面了,就不需要遍历最小值了
if(num>arr[i]){
//如果遍历出的数字大于当前的最小值就,就进行交换
num=arr[i];
index=i;
}
//否则就进行下一次循环
}
//进行数值交换
temp=arr[j];
arr[j]=arr[index];
arr[index]=temp;
}
}
}
插入排序
插入排序本质是自己先把数组中的一个数据(一般是第一个)当成一个有序表,数组中剩下的数据都是无序表,然后再遍历这个无序表中的数据,按顺序添加到这个有序表中,就实现了排序操作。要点:其中需要每次与有序列表比较都要把有序列表向后挪进。
一直重复上述操作,直到无序表中的数据被遍历结束,此时有序表中的数据也已然将传入要排序的数组排序完毕。
下面代码附上:
package com.liu.sortalgorithm;
import java.util.Arrays;
/**
* @author liuweixin
* @create 2021-09-09 21:35
*/
//插入排序的实现
public class InsertSortAlgorithm {
public static void main(String[] args) {
int[] arr = new int[20];
for (int i = 0; i < arr.length; i++) {
arr[i]= (int) (Math.random()*800000);
}
System.out.println("排序前:" + Arrays.toString(arr));
InsertSortAlgorithm.sort(arr);
System.out.println("排序后:" + Arrays.toString(arr));
}
/**
* 进行插入排序操作
*
* @param arr 要排序的数组
*/
public static void sort(int[] arr) {
int val;//从无序表中要插入到有序表中的数据
int index;//有序表中的最后一个数据的下标,也即无序表第一个数据的前一个数据的下标
for (int i = 1; i < arr.length; i++) {
val = arr[i];//获取要插入的数据
index = i - 1;//获取有序表中的最后一个数据的下标
while (index >= 0 && arr[index] >= val) {
//此循环是为了找到有序表中,比val小的值,并且将有序列表向后挪动
arr[index + 1] = arr[index];//将有序列表后移
index--;
}
//此时为找到插入的位置即index+1,将val赋值到有序列表中
arr[index + 1] = val;
}
}
}
希尔排序
希尔排序是插入排序的高效版,是对于插入排序的一种优化。当出现如果无序表中的数据太小,则需要把有序表向后挪进太多,效率较低,因此希尔排序应运而生,有效地解决了这个问题
希尔排序分为交换式与移位式,其实两种方式实现的差别大体不大,只是在组内值的排序中效率有所差异
这里我借用尚硅谷韩顺平老师的图,因为我觉得很形象。
意思就是说:我们先将数据分组,分组后对分组的数据进行排序,小的值放在左边,大的值放在右边,形成有序小组,随着分组的减小,即组内数据的增大,最终变为一个组,这个组囊括了所有数据,此时组内的数据已然接近有序,这就避免了大规模数据的移动。分组的方式避免了无序表中数值过小而引起整个数组的向后挪动
交换式实现希尔排序:
首先将数据分组,分成gap=arr.length()/2组,距离为gap的数据在同一组,然后这些组进行大小比较然后排序,类似于冒泡排序(将小的值放到前面,大的值放到后面,两者的位置的变动通过值得交换来实现,也就是交换式)。完成一次排序后,继续分组,组数为gap/2,然后又进行插入排序,如此反复,最终会获得一个有序序列。
代码附上
/**
* 交换式实现希尔排序,即通过交换值来实现插入排序
*
* @param arr 要排序的数组
*/
public static void sort(int[] arr) {
int temp;//辅助变量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {//对数组进行分组
for (int i = gap; i < arr.length; i++) {//此为遍历下标为gap后面的数据
for (int j = i - gap; j >= 0; j -= gap) {//此为遍历下标为gap前面的数据,即为了获得同一组的数据
if (arr[i] < arr[j]) {
//如果后面的数据比前面的数据小,则前后数据交换
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//否则数据就不动,继续进行下一个数据的比较
}
}
}
}
移位式实现希尔排序:
首先将数据分组,分成gap=arr.length()/2组,距离为gap的数据在同一组,然后这些组进行直接插入排序(在同一组中,设置一个数据为有序列表,剩下的数为无序列表,实现插入排序,在这插入排序的过程中,通过与有序表的最后一个数据进行比较,如果小于有序表的最后一个数据,则将整个有序列表向后挪动,这就是所谓的移位式)。完成一次插入排序后,继续分组,组数为gap/2,然后又进行插入排序,如此反复,最终会获得一个有序序列
下面代码附上:
/**
* 移位式实现希尔排序
*
* @param arr 要排序的数组
*/
public static void sort1(int[] arr) {
int temp;//辅助变量,进行记录
int index;//辅助变量,记录下标,帮助进行组内数据的循环
for (int gap = arr.length / 2; gap > 0; gap /= 2) {//同样也是需要对数据进行分组
for (int i = gap; i < arr.length; i++) {//这是分组后 后边的数据,进行遍历
temp = arr[i];//记录下要插入的数据
index = i - gap;//记录下与要插入数据同组别的前一个数据的下标
if (arr[i] < arr[i - gap]) {//后面的数据比前面的数据要小的话,则进行移位
while (index >= 0 && arr[i] < arr[index]) {
//则进行有序数据的向后挪动
arr[i]=arr[index];
index -= gap;//继续向前寻找同组的数据进行比较
}
//当循环结束,此时遍历到有序列表中的数据已经比arr[i]小
}
//此时index的索引已经多减了一个gap步长,所以需要先加个gap才是要插入的位置
index+=gap;
//此时将要插入的数据插入到有序列表中,最终实现了移位
arr[index] = temp;
}
}
}
下面是两种方式实现希尔排序的完整代码:
package com.liu.sortalgorithm;
import java.util.Arrays;
/**
* @author liuweixin
* @create 2021-09-09 22:38
*/
//希尔排序,希尔排序是插入排序的高效版本
//交换式与移位式的实现
public class DonaldShellSortAlgorithm {
public static void main(String[] args) {
int[] arr = new int[]{-1, 3, 5, 11414, 112, 426, 112, -2};
System.out.println("排序前:" + Arrays.toString(arr));
System.out.println();
// DonaldShellSortAlgorithm.sort(arr);//交换式
DonaldShellSortAlgorithm.sort1(arr);//移位式
System.out.println("排序后:" + Arrays.toString(arr));
}
/**
* 交换式实现希尔排序,即通过交换值来实现插入排序
*
* @param arr 要排序的数组
*/
public static void sort(int[] arr) {
int temp;//辅助变量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {//对数组进行分组
for (int i = gap; i < arr.length; i++) {//此为遍历下标为gap后面的数据
for (int j = i - gap; j >= 0; j -= gap) {//此为遍历下标为gap前面的数据,即为了获得同一组的数据
if (arr[i] < arr[j]) {
//如果后面的数据比前面的数据小,则前后数据交换
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//否则数据就不动,继续进行下一个数据的比较
}
}
}
}
/**
* 移位式实现希尔排序
*
* @param arr 要排序的数组
*/
public static void sort1(int[] arr) {
int temp;//辅助变量,进行记录
int index;//辅助变量,记录下标,帮助进行组内数据的循环
for (int gap = arr.length / 2; gap > 0; gap /= 2) {//同样也是需要对数据进行分组
for (int i = gap; i < arr.length; i++) {//这是分组后 后边的数据,进行遍历
temp = arr[i];//记录下要插入的数据
index = i - gap;//记录下与要插入数据同组别的前一个数据的下标
if (arr[i] < arr[i - gap]) {//后面的数据比前面的数据要小的话,则进行移位
while (index >= 0 && arr[i] < arr[index]) {
//则进行有序数据的向后挪动
arr[i]=arr[index];
index -= gap;//继续向前寻找同组的数据进行比较
}
//当循环结束,此时遍历到有序列表中的数据已经比arr[i]小
}
//此时index的索引已经多减了一个gap步长,所以需要先加个gap才是要插入的位置
index+=gap;
//此时将要插入的数据插入到有序列表中,最终实现了移位
arr[index] = temp;
}
}
}
}
快速排序:
快速排序其实是对于冒泡排序的一种改进。
思路为:先把要排序的数据以中间的数据为界(任何一个数都可以作为基准数,一般我们选择中间的数),分别左右遍历,当左边遍历出比该基准数大的值,右边遍历出比该基准数小的值,则将两数据交换,如此循环,最终会得到一组左边都比基准数小、右边都比基准数大的数据,将上述操作分别进行向左、向右递归,重复排序换值操作,则最终会获得一个有序的序列。
思路如下:
下面附上代码实现:
package com.liu.sortalgorithm;
import java.util.Arrays;
/**
* @author liuweixin
* @create 2021-09-10 19:05
*/
//快速排序
public class QuickSortAlgorithm {
public static void main(String[] args) {
int[] arr = new int[8];
for (int i = 0; i < arr.length; i++) {
arr[i]= (int) (Math.random()*800000);
}
System.out.println("排序前:" + Arrays.toString(arr));
System.out.println();
QuickSortAlgorithm.sort(arr, 0, arr.length - 1);
System.out.println("排序后:" + Arrays.toString(arr));
}
/**
* 快速排序操作
*
* @param arr 传入要排序的数组
* @param left 左边值的下标
* @param right 右边值的下边
*/
public static void sort(int[] arr, int left, int right) {
int l = left;//左下标
int r = right;//右下表
int mid = arr[(left + right) / 2];//找出数据的中间数
int temp;//辅助变量,帮助交换
while (l < r) {//当左边值的指针小于右边指针时,进入循环。否则则说明值遍历结束
while (arr[l] < mid) {//如果左边的值遍历出来比中间值小,就继续遍历下一个
l++;
}
while (arr[r] > mid) {//如果右边的值遍历出来比中间值大,就继续遍历下一个
//如果右边遍历完,左边未遍历完。此时arr[r]的值仍一直是arr[r]=mid,故一直不会>mid,此时可以一直借助arr[r]=mid与左边遍历的数据比较
//当发生两数交换,则同理,arr[l]=mid,此时,arr[r]的指针r,就有可能右移
r--;
}
if (l >= r) {//如果上述循环结束,l>=r,则说明此时左边所有值都比中间值小,右边所有值都比中间大,此时不需要交换,直接结束循环
break;
}
//否则进行交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//如果一边遍历完,另一边没遍历完,则上述 值的交换 会交换到mid值,所以我们需要对交换后的值进行判断
if (arr[l] == mid) {
//如果交换结束,arr[l]与中间值相等,则说明,此时右边的值已然遍历完,则r前移
r--;
}
//如果交换结束,arr[r]与中间值相等,则说明,此时基准值左边值已然遍历完,则l后移
if (arr[r] == mid) {
l++;
}
//然后进行下一轮循环
}
//如果l==r,必须l++,r--,否则会出现栈溢出,因为要向左向右递归
if (l == r) {
l++;
r--;
}
if (left < r) {
//向左递归
//此时左数据的起始下标为left,右下标即为刚经过第一次排序后的右指针的位置
sort(arr, left, r);
}
if (right > l) {
//向右递归
//左下标即为刚经过第一次排序后的左指针的位置,此时右数据下标为全部数据的最后一个数据的指针
sort(arr, l, right);
}
}
}
归并排序
归并排序主要是利用分治的思想进行排序。分治的思想主要是,先拆分成一个小问题,解决后再合并起来再解决,先解决小问题,再慢慢合成大问题,简化了解决的难度。
排序的主要思想是:先把一组数据进行拆分,拆分成不可拆分后,如最终拆成一个一个的数据,然后将相邻两个元素进行排序,然后将这两个元素合并;然后再与相邻的合并的两个元素进行排序,排序完合并成4个元素,再去与相邻的合并完的四个元素排序…..周而复始,最终合并成一组大的数据,此时这个大的数据组即为有序的。
这里同样借助韩老师的图,尚硅谷韩老师的图确实画的很好。
通过分,而后治、再合!完成了数据的排序
而我们这里的合是如何实现的呢?我以图上的[4,8]与[5,7]举例
最后再把临时数组中的数据,遍历到原数组(即传入的要排序的数组中),这就实现了归并排序。
下面附上代码实现:
package com.liu.sortalgorithm;
import java.util.Arrays;
/**
* @author liuweixin
* @create 2021-09-11 9:36
*/
//归并排序
public class MergeSortAlgorithm {
public static void main(String[] args) {
int[] arr = new int[8];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * 800000);
}
System.out.println("排序前:" + Arrays.toString(arr));
System.out.println();
int[] temp = new int[arr.length];
MergeSortAlgorithm.MergeSort(arr, 0, arr.length - 1, temp);
System.out.println("排序后:" + Arrays.toString(arr));
}
/**
* 进行分+合+治
*
* @param arr 要排序的数据
* @param left 数据左边的第一个下标
* @param right 数据最后一个的下标
* @param temp 临时储存结果的数组
*/
public static void MergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {//分结束的临界点,当left>=right时,意味着此时不可再分
int mid = (left + right) / 2;
//向左递归,分
MergeSort(arr, left, mid, temp);
//向右递归,分
MergeSort(arr, mid + 1, right, temp);
//分完后,进行合治
sort(arr, left, mid, right, temp);
}
}
/**
* 处理合、治的操作
*
* @param arr 要排序的数组
* @param left 数据中第一个数据的下标
* @param mid 数据中间索引
* @param right 数据中最后一个数据的下标
* @param temp 临时的数组,用于储存最后的排序结果
*/
public static void sort(int[] arr, int left, int mid, int right, int[] temp) {
int i = left;//初始化i,左边有序序列的初始索引
int j = mid + 1;//右边有序序列的索引
int t = 0;//记录临时数组的下标
while (i <= mid && j <= right) {// 合 : 遍历左边有序数组和右边有序数据
if (arr[i] <= arr[j]) {
//左边数据比右边数据小,则先把小的数据添加到临时数组中
temp[t] = arr[i];
i++;//左边数据下标后移
t++;//临时数组的下标后移
} else {
//右边数据比左边数据小,则先把小的数据添加到临时数组中
temp[t] = arr[j];
j++;//右边数据下标后移
t++;//临时数组的下标后移
}
}
//如果左边有剩下的数据,则直接加入到临时数组中
while (i <= mid) {
temp[t] = arr[i];
i++;
t++;
}
//如果右边有剩下的数据,则直接加入到临时数组中
while (j <= right) {
temp[t] = arr[j];
j++;
t++;
}
t = 0;//重置t值
int leftIndex = left;//数据中的第一个数据的索引
//最后把临时数据中的数据,转回给要排序的数据
while (leftIndex <= right) {//从左向右
arr[leftIndex] = temp[t];
t++;
leftIndex++;
}
}
}
基数排序:
基数排序即为桶排序的拓展,基数排序是一种稳定的快速的排序。其局限为只能处理非负数。
其实现思路是:先把每个数据的个位数找出来,然后按照其数值,分别放到0-9的桶中,假设说我的个位数是3,那我这个数就放到第3号桶中;将所有的数据安置完,然后再把所有的数据按顺序遍历出来,重置要排序的数组(意思是指按顺序遍历桶内的数,放到传进来的数组中),这时候该数组就会得到一个按照个位数的数值排序的一个数组。然后我们再取所有数据的十位数(若没有该为数,则为0),同样如上排序;然后再百位、千位….(以最大数值的数的最高位数为准)。最终我们就能够获得一个有序的数组了。
这里同样借助韩老师的图进行图讲解。
先是对所有数据的个位数放桶处理,然后按照顺序,把桶内的数据取出到原数组中(要排序的数组)。
然后再进行十位数的排序:
同理 ,按照顺序,把桶内的数据取出到原数组中(要排序的数组)。
紧接着,再按百位上的数进行放桶处理
最终我们再按顺序取出数据,就获得了一个有序的序列。
下面附上代码实现:
package com.liu.sortalgorithm;
import java.util.Arrays;
/**
* @author liuweixin
* @create 2021-09-11 10:40
*/
//基数排序
public class RadixSortAlgorithm {
public static void main(String[] args) {
int[] arr = new int[20];
for (int i = 0; i < arr.length; i++) {
arr[i]= (int) (Math.random()*800000);
}
System.out.println("排序前:" + Arrays.toString(arr));
System.out.println();
RadixSortAlgorithm.sort(arr);
System.out.println("排序后:" + Arrays.toString(arr));
}
/**
* 进行基数排序
*
* @param arr 要排序的数组
*/
public static void sort(int[] arr) {
//首先获得要排序的数据的最大位数
int max = arr[0];//记录最小的数,先假设第一个数据是最小的数
for (int i = 1; i < arr.length; i++) {
if (max < arr[i]) {//如果遍历出的数据比max大,则交换
max = arr[i];
}
}
//遍历结束,获得最小的数,并获得去最大位数
int max_length = (max + "").length();//借助字符串获得其最大位数
//创建二维数组,即桶,行代表0-9,即代表位数上的数字,列代表加入桶中的数据
//列的个数是数组的长度的原因是:以防出现极端情况,在某位数上,出现所有的数据都是放在同一个桶,避免数组角标越界
int[][] bucket = new int[10][arr.length];
//我们需要记录下每个桶中加入数据的数量,可以借助一个一维数组来实现
int[] count = new int[10];
//举例:当某数的某位数为3,bucket[3][count[3]],count[3]初始值为0,加完之后count[3]++,此时count[3]的值为1
// 这样我们就可以记录下该桶的个数
//然后我们进行对传入的数组的数据的遍历
for (int i = 0, n = 1; i < max_length; i++, n *= 10) {//对数据进行位数处理
for (int j = 0; j < arr.length; j++) {//对每个数据进行判断
int data = (arr[j] / n) % 10;//获得对应位数上的数据
//将该数据加入到对应的桶中
bucket[data][count[data]] = arr[j];
count[data]++;
}
//此时已经将数据添加到桶里,然后再按顺序返回到数组中
int index = 0;//辅助指针,指向数组的第一位
//将桶里的数据添加到数组中
for (int j = 0; j < bucket.length; j++) {
//j代表是桶的行,即桶的数字
for (int k = 0; k < count[j]; k++) {
//大于0代表该桶中有数据,将数据添加回原数组中
arr[index++] = bucket[j][k];
}
//当数据填完到原数组中,此时对应的count[j]需要置零,若不置零,下批数据仍然用这个count[j]的值,则会出错
count[j]=0;
}
}
}
}
总结:
这些排序算法细节点还是很多的,还是需要自己仔细敲,仔细debug,才能够真正弄懂其是如何实现的,有些细节点在图文解释中很难解释出来,只有在代码中才能得到更好的诠释。
相信我们大家对于代码的理解性比对文字的理解性要好,所以看代码去思索实现也是很重要的,不能只看了图文就觉得自己已经理解了,这其实是很片面的,真的需要投入到代码中,去理解每一步代码的作用,我的代码实现方式跟韩老师的不尽相同,只是思路是一样的,因为这是我自己思索着敲的,虽然我的代码不及韩老师的优雅、简练,但是我在几乎每一步代码中,都加了注释,这也是我个人的理解,也许我的代码对于新手来说,相比于韩老师的代码更容易理解。
大家看完理解后,也可以自己去尝试敲下来。这对大家十分有益的。最后希望大家都能有所收获