列举了七种常用的排序算法的思路及代码实现
冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序、桶排序
代码上有很详细的注解,跟着代码的思路走,能够轻松理解这些算法
以后会开始刷leetCode的算法题,有趣的题目会写下来与大家分享,欢迎关注和讨论
冒泡排序
冒泡排序(bubble sorting)的基本思想是:通过对需要排序的序列从前向后(下标较小的元素开始),依次比较相邻元素的值,如果发现逆序的元素则交换,使值较大(或者值较小)的元素逐渐从前移到后
代码实现:
使用双重for循环来进行冒泡排序,外层for循环是为了将遍历整个数组,所以循环的次数是(数组的长度-1)次,因为数组的最后一位是不需要进行比较的。内层的for循环是将元素进行比较和换位
package test;
import java.util.Arrays;
public class DataStructureTest {
public static void main(String[] args) {
//将{3, 9, -1, 10, -2}从小到大进行排序
int arr[] = {3, 9, -1, 10, -2};
int temp = 0;//临时变量
for(int j = 0; j < arr.length - 1; j++) {
for(int i = 0; i < arr.length - 1 - j; i++) {
if(arr[i] > arr[i + 1]) {//如果前面的数比后面的数要大,就交换
temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
}
}
System.out.println("第" + (j+1) + "次排序后的数组");
System.out.println(Arrays.toString(arr));//输出数组
}
}
}
选择排序
选择排序(select sorting)是从需要排序的数据中,按照指定的规则选出某一元素,再按照规则交换位置后,达到排序的目的
选择排序的基本思想是:第一次从arr[0] ~ arr[n-1]中选出最小值,与arr[0]交换,第二次从arr[1]~ arr[n-1]中选出最小值,与arr[1]交换……,第i次从arr[i-1]~ arr[n-1]中选出最小值,与arr[i-1]交换……,第n-1次从arr[n-2]~ arr[n-1]中选出最小值,与arr[n-2]交换,总共通过n-1次排序,得到一个按排序码从小到大排列的有序序列
package test;
import java.util.Arrays;
public class DataStructureTest {
public static void main(String[] args) {
int[] arr = { 44, 33, 55, 22, 11, 22};
System.out.println("排序前");
System.out.println(Arrays.toString(arr));
selectSort(arr);
System.out.println("排序后");
System.out.println(Arrays.toString(arr));
}
// 选择排序,数组为[44,33,55,22]
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;// 假定最小值的索引为i,即为第i个数据下标
int min = arr[i];//假定最小值为arr的第i个数据
/*
* j从minIndex的下一个开始比较,所以for循环从1开始
* 同时要注意遍历的数组长度,因为j从1开始遍历,所以想要遍历arr.length-1个数据,只需使j< arr.length
*/
for (int j = 1 + i; j < arr.length; j++) {
if (min > arr[j]) {// 假定的第一个数据为最小值,并不是最小值
min = arr[j];
minIndex = j;
}
}
//经过for循环,就已经找到了最小值及其索引,将最小值与arr[i]进行交换
if (minIndex != i) {//如果最小值的索引为i,说明当前值就是最小值,没必要交换
//将最小值与arr[i]进行交换
arr[minIndex] = arr[i];
arr[i] = min;
}
}
}
}
插入排序
对需要排序的元素以插入的方式找该元素的适当位置,以达到排序的目的
插入排序的基本思想是,把n个待排序的元素看成一个有序表和一个无序表,开始时有序表只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把其排序码依次与有序表元素的排序码进行比较,把它插入到有序表的适当位置,使之成为新的有序表
package com.sort;
import java.util.Arrays;
public class InsertSort {
public static void main(String[] args) {
int[] arr = {44,33,55,22};
insertSort(arr);
System.out.println(Arrays.toString(arr));
}
//插入排序,从小到大排序
public static void insertSort(int[] arr) {
/*
* 要注意,i从1开始遍历,因为把数组的第一个元素作为有序表,以此为基准进行后续比较和排序
* 因为数组的第一个元素不需要遍历,所以遍历的长度为数组的长度-1
*/
for(int i = 1; i < arr.length; i++) {
int insertNum = arr[i];//定义待插入的数,为33
/*
* 定义待插入的索引,为待插入数的前一个数的下标
* 因为遍历是从第i个数据开始的,第i个数据要向前插入有序表
* 所以先和第i-1个的数据进行比较
*/
int insertIndex = i - 1;
/*
* 给insertNum找到插入的位置
* 1.insertIndex >= 0保证给insertNum找插入位置的时候,不越界
* 2.insertNum < arr[insertIndex],如果待插入的数小于前一个数,说明还没有找到插入位置
* 3.就需要将前一个数arr[insertIndex]后移
*/
while(insertIndex >= 0 && insertNum < arr[insertIndex]) {
/*
* 将前一个数arr[insertIndex]后移,此时数组会变成{44,44,55,22}
* 但是本来的arr[1]=33已经保存在insertNum之中,所以数据并没有丢失
*/
arr[insertIndex + 1] = arr[insertIndex];
insertIndex--;//再把索引往前移动,会再走一次while循环,进行比较,直到找到插入位置
}
/*
* 所以退出while循环时,说明此时插入的位置已经找到,此时的插入位置是insertIndex+1
* 因为退出while循环的条件是,insertNum > arr[insertIndex]或者insertIndex < 0,即要插入的数比arr[insertIndex]要大或者已经到达数组的第一个数据arr[insertIndex]的位置
* 所以要插入的数的下标应该是insertIndex+1,即要插入的数放在arr[insertIndex]之后
*/
arr[insertIndex + 1] = insertNum;
}
}
}
希尔排序
希尔排序也是一种插入排序,是简单插入排序经过改进之后的一个更为高效的版本,也称为缩小增量排序
希尔排序是把记录按下标的一定增量进行分组,对每组使用直接插入排序算法排序,随着增量的逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件就被分为一组,算法终止
希尔排序思路
- 初始增量=数组的长度/2,在确定初始增量后,分组的依据是,每隔增量-1的数据,就分为一组,并进行排序
- 缩小的增量=分组的组数/2,在确定缩小增量之后,每隔增量-1的数据,就分为一组,并进行排序
- 最后经过数次缩小增量排序,整个数组被分为唯一一组,相比初始数组,是更有序的数组
- 最后对这个唯一的数组进行两两比较和交换
要注意理解增量的概念
package com.sort;
import java.util.Arrays;
public class ShellSort {
public static void main(String[] args) {
int[] arr = {8,9,1,7,2,3,5,4,6,0};
shellSort(arr);
}
//希尔排序,从小到大排序,对有序序列在插入时采用交换法
public static void shellSort(int[] arr) {
int temp = 0;
//希尔排序的第一轮排序,将10个数据分成了5组
for(int i = 5; i < arr.length; i++) {//i = 5开始for循环,i是分组的组数
/*
* 遍历各组中所有的元素,共5组,每组有两个元素,步长为5
*j = j-5不是为了使for循环能再来一次,而是要保证for循环只进行一次
* 因为这是第一次分组,每组只有两个数据,此for循环是为了能对这两个数据进行排序
* 假如数组有14个数据,那么for循环的条件应该是int j = i - 7; j >= 0; j = j - 7
* 当然i = 7,从7开始遍历
*/
for(int j = i - 5; j >= 0; j = j - 5) {
if(arr[j] > arr[j + 5]) {//如果第j个元素的值要大于第j+5个元素的值,就进行交换
temp = arr[j + 5];
arr[j + 5] = arr[j];
arr[j] = temp;
}
}
}
System.out.println("希尔排序第一轮排序" + Arrays.toString(arr));
/*
* 从希尔排序的第一轮排序,推导到用for循环进行自动分组
* 第一次分组是arr.length/2,即数组的长度除以2
* 第二次分组就是第一次分组的组数/2
* 最后直到只有一组,1/2=0,就不会进入for循环
*/
for(int gap = arr.length/2; gap > 0; gap = gap/2) {
for(int i = gap; i < arr.length; i++) {//i = gap开始for循环,gap是每次分组的组数
/*
* 遍历各组中所有的元素(共gap组,每组有arr.length/gap个元素,步长为gap
* 到最后gap=1的时候,就两两比较并交换,比如[0, 2, 1, 4, 3, 5, 7, 6, 9, 8]此时gap=1
* 先0和2比较,0<2则不交换
* 2和1比较,2>1,交换,[0, 1, 2, 4, 3, 5, 7, 6, 9, 8]
* 2和4进行比较,2<4,不交换
* 4和3进行比较,4>3,交换[0, 1, 2, 3, 4, 5, 7, 6, 9, 8]
* 以此类推
* 最后所有的数排序完毕
*/
for(int j = i - gap; j >= 0; j = j - gap) {
if(arr[j] > arr[j + gap]) {
temp = arr[j + gap];
arr[j + gap] = arr[j];
arr[j] = temp;
}
}
}
System.out.println("希尔排序过程:" + Arrays.toString(arr));
}
}
}
快速排序
快速排序(Quick Sort)是对冒泡排序的一种改进,基本思想是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据要小,然后再按照此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
快速排序的思路就是:
- 以最右边的数为基准,向左遍历,比基准大的数放在基准的右边,比基准小的数放在基准的左边,结果会分成两个部分
- 左边比基准都要小的部分,再次以最右边的数为基准,向左遍历,比此基准小的数放在基准的左边,比此基准大的数放在基准的右边…以此类推,直到左边的部分排序完成
- 右边的部分同理
- 基准不一定要找最左边或者最右边的数,也可以找中间的数
package com.sort;
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args) {
int[] arr = {-9,77,0,23,-56,70,-111,90,88,-33};
System.out.println("原始数组:"+ Arrays.toString(arr));
quickSort(arr, 0, arr.length - 1);
System.out.println("快速排序后数组"+Arrays.toString(arr));
}
public static void quickSort(int[] arr, int left, int right) {
int l = left;//左下标,用于向左遍历
int r = right;//右下标,用于向右遍历
//中间值,即基准值
int pivot = arr[(left + right) / 2];
int temp = 0;//临时变量,交换时使用
/*
* while循环,while循环的目的是让比pivot基准值小的放在左边
* 比pivot基准值大的放在右边
*/
while(l < r) {
/*
* 在pivot的左边一直找,找到大于等于pivot的值的数,才退出while循环
* l默认从最左边开始
*/
while(arr[l] < pivot) {
l = l + 1;//如果没找到,则l+1并再次循环
}
/*
* 在pivot的右边一直找,找到小于等于pivot的值的数,才退出while循环
* r默认从最右边开始
*/
while(arr[r] > pivot) {
r = r - 1;//如果没找到,则r-1并再次循环
}
/*
* 退出while循环的时候
* 如果l>=r,说明已经遍历完了左右两边,并且左边已经全是小于等于pivot的值
* 右边全是大于等于pivot的值,此时可以退出大的while循环
*/
if(l >= r) {
break;
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
/*
* 交换完过后,还需要考虑arr[l]和arr[r]的值等于pivot的值的情况
* 比如说[l, pivot, r]=[1, 1, 2],已经到了最后一步交换
* 此时l的while循环不会通过,arr[l]=1,r的while循环会通过,r=r-1,arr[r]=1
* 经过交换过后,[l, pivot, r]=[1, 1, 2],会发现数组还是原来的样子,此时l<r,不构成退出的条件
* 依然还会继续while循环,变成死循环,所以如果交换完之后,发现arr[l]=pivot,就使r--(r前移)
* 这样就会使得l=r,while循环退出
*/
if(arr[l] == pivot) {
r = r - 1;
}
if(arr[r] == pivot) {
//注意,l应该是l = l + 1
l = l + 1;
}
}
//如果l == r,必须使l++,r--,否则会出现栈溢出
if(l == r) {
l = l + 1;
r = r - 1;
}
/*
* 向左递归,要注意左递归的条件,因为经过了第一次的快速排序
* 原数组[-9,77,0,23,-56,70]变成了[-9, -56, 0, 23, 77, 70],此时l在-56的位置,r在23的位置
* 所以向左递归的条件,是left < r,即取到了[-9, -56, 0],递归完之后变为有序[-56, -9, 0]
*/
if(left < r) {//left可能会大于r,此时退出递归循环
quickSort(arr, left, r);
}
//向右递归同理
if(right > l) {//right可能会小于l,此时退出递归循环
quickSort(arr, l, right);
}
}
}
归并排序
归并排序(Merge Sort)是利用归并的思想实现的排序问题,该算法采用经典的分治(divide and conquer)策略,分治法将问题分成一些小的问题然后递归求解,治则体现在将分的阶段得到的答案修补在一起
package com.sort;
import java.util.Arrays;
public class MergeSort {
public static void main(String[] args) {
int[] arr = {8,4,5,7,1,3,6,2};
//临时数组
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;//中间索引
/*
* 向左递归进行分解,此时arr相当于[left,xxx,mid,xxx,right]
* 取[left,mid]部分的进行分解
*/
mergeSort(arr, left, mid, temp);
/*
* 向右递归进行分解
* 要注意,mid划给了左边,所以向右递归的时候,左界限为mid + 1
*/
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) {
System.out.println("xxxx");
int i = left;//初始化i,表示左边有序序列的初始索引
int j = mid + 1;//初始化j,表示右边有序序列的初始索引
int t = 0;//t是指向temp数组的当前索引
/*
* 1.先把左右两边的数据(有序)按照规则填充到temp数组
* 直到左右两边的有序序列,有一边处理完毕为止
* 2.把有剩余数据的一边依次全部填充到temp数组
* 3.将temp数组的元素,拷贝至arr数组
*/
while(i <= mid && j <= right) {//左右两边都还没有处理完
/*
* 如果左边的有序序列的当前元素(下标为i),小于或者等于右边序列的当前元素(下标为j)
* 就将左边的当前元素(下标为i),填充到temp数组,并使temp数组的下标后移
*/
if(arr[i] <= arr[j]) {
temp[t] = arr[i];
i++;
t++;
//反之,就将右边的当前元素(下标为j),填充到temp数组,并使temp数组的下标后移
}else if(arr[i] > arr[j]) {
temp[t] = arr[j];
j++;
t++;
}
}
/*
* 把有剩余数据的一边的数据依次全部填充至temp
* 从while循环出来,就满足i > mid 或者 j > right两个条件中的一个
* 即有一边已经遍历完成
* 所以应该对未完成的一边做判断,要注意while中的条件就是i <= mid或者j <= right
* 因为假如是i没有遍历完,那么上面的while最后一次遍历的一定是j,然后j++不满足上面的while条件
* 但是i没有进行遍历,也就没有i++,所以是i <= mid
*/
while(i <= mid) {//i所在的左边的有序序列没有遍历完,将其中元素全部填充到temp
temp[t] = arr[i];
i++;
t++;
}
while(j <= right) {//j所在的右边的有序序列没有遍历完,将其中元素全部填充到temp
temp[t] = arr[j];
j++;
t++;
}
/*
* 将temp数组的元素拷贝至arr,但是注意,并不是每次都拷贝所有
* 因为arr = {8,4,5,7,1,3,6,2},在治的时候,会进行合并和排序,
* 第一次合并,会变成4个部分[4,8],[5,7],[1,3],[2,6]
* 第二次合并,会变成2个部分[4,5,7,8],[1,2,3,6]
* 第三次合并,才会变成有序的最后的数组[1,2,3,4,5,6,7,8]
* 所以在将temp数组的元素拷贝至arr时,会经过7次拷贝,第一次tempLeft=0,right=1,对应[4,8]部分
* 第二次tempLeft=2,right=3,对应[5,7]部分,第三次tempLeft=0,right=3,对应[4,5,7,8]部分
* 第四次[1,3]部分,第五次[2,6]部分,第六次[1,2,3,6]部分
* 最后一次,也就是第七次tempLeft=0,right=7,对应[1,2,3,4,5,6,7,8]
*/
t = 0;
int tempLeft = left;
System.out.println("tempLeft=" + tempLeft + " right=" + right);
while(tempLeft <= right) {
arr[tempLeft] = temp[t];
t++;
tempLeft++;
}
}
}
桶排序
- 基数排序(radix sort)属于分配式排序,又称桶排序,通过键值的每个位的值,将要排序的元素分配至某些桶中,达到排序的作用
- 基数排序是属于稳定性的排序,基数排序法是效率高的稳定性排序法
- 基数排序是桶排序的扩展
基数排序基本思想:将所有待比较的数统一为同样的数位长度,数位较短的数前面补零,然后从最低位开始,依次进行一次排序,这样从最低位排序一直到最高位排序完成后,数组就变成了一个有序序列
桶排序的思路图解:
桶排序的代码实现
package com.sort;
import java.util.Arrays;
public class RadixSort {
public static void main(String[] args) {
int[] arr = { 53, 3, 542, 748, 14, 214 };
radixSort(arr);
}
//基数排序方法
public static void radixSort(int[] arr) {
//第一轮排序,针对每个元素的个位进行排序处理
/*
* 1.定义一个二维数组,表示10个桶,每个桶就是一个一维数组
* 2.为了防止在放入数的时候,数据溢出,每一个一维数组(桶),大小为arr.length
* 3.所以基数排序也是空间换时间的经典算法
*/
int[][] bucket = new int[10][arr.length];
/*
* 为了记录每个桶中实际存放了多少个数据,定义一个一维数组来记录各个桶每次放入的数据个数
* 即bucketElementCounts[0],记录的就是bucket[0]代表的桶的放入数据的个数
*/
int[] bucketElementCounts = new int[10];
//第一轮(针对每个元素的个位进行排序处理)
for(int j = 0; j < arr.length; j++) {
//取出每个元素的个位的数,用取模的方式来获取个位数
int digitOfElement = arr[j] % 10;
/*
* 放入到对应的桶中
* bucket[digitOfElement][bucketElementCounts[digitOfElement]]中
* [digitOfElement]表示放在哪个桶,因为取模得到了个位数就对应相应的桶
* [bucketElementCounts[digitOfElement]]表示放在该桶的第几个元素位置
* 比如digitOfElement=7,[bucketElementCounts[7]]就表示[0,0,0,0,0,0,x,0,0,0],x表示放置的位置,x值为0
* 然后bucketElementCounts[digitOfElement]++,为了下一次在7的桶存入数据时
* bucket[digitOfElement][0]会变成bucket[digitOfElement][1]
*/
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
bucketElementCounts[digitOfElement]++;
}
//按照桶的顺序(一维数组的下标),依次取出数据,放入原来数组
int index = 0;
//遍历每一个桶,并将桶中的数据放入到原数组
for(int k = 0; k < bucketElementCounts.length; k++) {
//如果桶中有数据,放入到原数组
if(bucketElementCounts[k] != 0) {
//循环第k个桶,即二维数组bucket中的第k个一维数组
for(int l = 0; l < bucketElementCounts[k]; l++) {
//从bucket取出元素,放到arr中
arr[index] = bucket[k][l];
index++;
}
}
/*
* 在每一轮处理后,需要将每个桶对应的bucketElementCounts[k]清零
* 因为下一轮的bucketElementCounts[k]的值是需要从零开始重新进行累计的
* 而不是在第一轮的基础上进行累计
*/
bucketElementCounts[k] = 0;
}
System.out.println("第一轮对个位的排序处理" + Arrays.toString(arr));
/*
* 根据第一轮的过程,可知第二轮,第三轮...对数的处理方式,可以写一个最终的基数排序的代码
* 需要先得到数组中最大的数的位数
*/
int max = arr[0];//假设第一个数就是最大数
for(int i = 1; i < arr.length; i++) {
if(arr[i] > max) {
max = arr[i];
}
}
/*
* for循环之后,已经找到了最大数为max
* 将max与空字符串拼接,再用length方法得到最大数的位数,来决定循环的轮数
*/
int maxLength = (max + "").length();
index = 0;
for(int i = 0, n = 1; i < maxLength; i++, n = n * 10) {
//对每个元素的对应位数进行排序处理,第一次是个位,第二次是十位...以此类推
for(int j = 0; j < arr.length; j++) {
//取出每个元素需要的位数,用取模的方式来获取位数
int digitOfElement = arr[j] /(1 * n) % 10;
//将数据放入到对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
bucketElementCounts[digitOfElement]++;
}
//按照桶的顺序(一维数组的下标),依次取出数据,放入原来数组
index = 0;
//遍历每一个桶,并将桶中的数据放入到原数组
for(int k = 0; k < bucketElementCounts.length; k++) {
//如果桶中有数据,放入到原数组
if(bucketElementCounts[k] != 0) {
//循环第k个桶,即二维数组bucket中的第k个一维数组
for(int l = 0; l < bucketElementCounts[k]; l++) {
//从bucket取出元素,放到arr中
arr[index] = bucket[k][l];
index++;
}
}
//在每一轮处理后,需要将每个桶对应的bucketElementCounts[k]清零
bucketElementCounts[k] = 0;
}
System.out.println("第" + (i+1) +"轮的排序处理" + Arrays.toString(arr));
}
}
}