插入排序
插入排序的基本思想是,将N个待排序元素分为一组有序表和一个无序表,一开始有序表只有一个元素,无序表中有N-1个元素,排序过程中每次取无序表的第一个元素依次与有序表的元素进行对比,插入到适当的位置使之成为新的有序表
我们可以看到在插入排序中排序需要进行的轮次是N-1轮(第一个元素默认有序),那么接下来看这个例子的第一轮的代码实现,以及代码解释
java
import java.util.Arrays;
public class Sort {
public static void main(String[] args) {
int[] arr = {101,34,119,1};
InsertSort(arr);
System.out.println(Arrays.toString(arr));
}
//插入排序
public static void InsertSort(int[] arr){
//原始数据:{101,34,119,1}
//第一轮 目标数据{34,101,119,1}
//定义待插入的数
int insertVal = arr[1];
int insertIndex = 1-1; //arr[1]前面这个数的下标索引
while(insertIndex>=0 && insertVal<arr[insertIndex]){
//1.insertIndex>=0保证给insertVal找寻插入位置不越界
//2.insertVal<arr[insertIndex]表示待插入的数还没有找到位置(从小到大排序)
//3.那么就需要将arr[insertIndex]后移,继续寻找知道找到合适的位置即大于前面的某个数,或者不满足1的条件,即为最小数
arr[insertIndex+1] = arr[insertIndex];//可以把这里看做是变成了 --> {101,101,119,1}
insertIndex--; //将insertVal与insertIndex前面的数再进行对比
}
//当退出while循环表示insertVal找到合适的位置 insertIndex+1
//为什么要加一,如果在序列中间找到合适的位置那么一定是insertIndex后面的那一个位置
//如果是序头找到的合适的位置(即最小数),那么insertIndex--后一定是越界的-1,那么他就是0的位置,所以需要+1
arr[insertIndex+1] = insertVal;
}
}
这就是得到的第一轮的结果,那么第二、第三轮也是一样如此操作,我们只需使用外部循环条件即可实现
ini
import java.util.Arrays;
public class Sort {
public static void main(String[] args) {
int[] arr = {101,34,119,1};
InsertSort(arr);
}
//插入排序
public static void InsertSort(int[] arr){
for(int i = 1;i<arr.length;i++){ //这里的arr.length不需要-1因为我们是从1下标开始
int insertVal = arr[i];
int insertIndex = i-1;
while (insertIndex>=0 && insertVal<arr[insertIndex]){
arr[insertIndex+1] = arr[insertIndex];
insertIndex--;
}
arr[insertIndex+1] = insertVal;
System.out.println("第"+i+"轮执行结果");
System.out.println(Arrays.toString(arr));
}
}
}
希尔排序(缩小增量排序)
在进行希尔排序的介绍之前我们先来看看插入排序的一些问题,现在我们给出这样一组数据[2,3,4,5,6,1], 那么他在进行插入排序时会产生很多次数的位移
对于这种极端情况,那么插入排序就显得效率低下,而希尔排序也是插入排序的一种,属于优化后的插入排序
希尔排序的思想是把记录按下标的一定增量进行分组,对每组进行插入排序进行排序,随着增量的逐渐减小,每组包含的元素越来越多,当增量缩小为1时,整个数组被分为一组,进行插入排序后则有序
这种是交换法的希尔排序,图中的进行插入排序其实不是真正的直接插入排序,而只是对比两个数进行交换
这就是希尔排序的图解,接下来我们来看代码实现
ini
import java.util.Arrays;
public class MyShellSort {
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个元素分为arr.length/2=5组
for (int i = 5; i < arr.length; i++) {
for (int j = i-5; j >=0; j-=5) {
//如果当前元素大于加上增量后的那个元素,进行交换
if (arr[j] > arr[j+5]){
temp = arr[j];
arr[j] = arr[j+5];
arr[j+5] = temp;
}
}
}
System.out.println("第一轮希尔排序后的结果:"+ Arrays.toString(arr));
//第二轮希尔排序
//第二轮排序,将10个元素分为(arr.length/2)/2=2组
for (int i = 2; i < arr.length; i++) {
for (int j = i-2; j >=0; j-=2) {
//如果当前元素大于加上增量后的那个元素,进行交换
if (arr[j] > arr[j+2]){
temp = arr[j];
arr[j] = arr[j+2];
arr[j+2] = temp;
}
}
}
System.out.println("第二轮希尔排序后的结果:"+ Arrays.toString(arr));
//第三轮希尔排序
//第三轮排序,将10个元素分为((arr.length/2)/2)/2=1组
for (int i = 1; i < arr.length; i++) {
for (int j = i-1; j >=0; j-=1) {
//如果当前元素大于加上增量后的那个元素,进行交换
if (arr[j] > arr[j+1]){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
System.out.println("第三轮希尔排序后的结果:"+ Arrays.toString(arr));
}
}
根据希尔三次排序我们可以摸索到希尔排序的循环规则,接下来我们将其整合起来
ini
import java.util.Arrays;
public class MyShellSort {
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; //定义临时变量,用于交换
int count = 0;
for(int gap = arr.length/2;gap>0;gap/=2){
for (int i = gap; i < arr.length; i++) {
for (int j = i - gap; j >= 0; j -= gap) {
//如果当前元素大于加上增量后的那个元素,进行交换
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
System.out.println("第"+(++count)+"轮希尔排序后的结果:" + Arrays.toString(arr));
}
}
}
我们看这段代码可以发现他嵌套了三个for循环,可想而知他的效率肯定是不高的,而介绍这种交换法的希尔排序主要是让我们理解增量这个概念,接下来我们来学习希尔排序移动法(进行直接插入排序)
移动法的希尔排序就是在分组后,对各组进行排序不是使用简单的交换,而是进行直接插入排序,即找到了合适的位置才插入(交换法效率低下的原因就是因为无脑进行交换,导致交换量过大)
ini
import java.util.Arrays;
public class MyShellSort {
public static void main(String[] args) {
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
System.out.println("希尔排序前的数组"+Arrays.toString(arr));
ShellSort(arr);
System.out.println("希尔排序后的数组"+Arrays.toString(arr));
}
//使用逐步推导的方式进行分析希尔排序
//交换法希尔排序
public static void ShellSort(int[] arr) {
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]; //arr[j]这个待插入数的前一个数向后挪
j -= gap;
}
//当退出while循环后,说明找到了合适的位置了
arr[j] = temp;
}
}
}
}
}
这就是移动法的希尔排序中间的排序逻辑和插入排序类似,只是多引出了gap增量这个概念
冒泡排序
冒泡排序的基本思想是,对待待排序序列从前向后,依次比较相邻元素的值,如果发现逆序则进行交换,使值较大的元素逐渐从前移向后部(从小到大排序),就像水底的气泡向上冒
接下来给出一幅图理解冒泡排序的过程
在这张冒泡排序图解中我们可以发现,一共进行数组长度-1次循环,每一趟都使得当趟最大的值回到适合的位置,并且每趟排序的次数都在逐渐减少(因为已经有元素到了适合的位置)
接下来就来看看冒泡排序的代码演示,为了方便理解我将一步一步拆分
ini
```
import java.util.Arrays;
public class MyBubbleSort {
public static void main(String[] args) {
int[] arr = {3,9,-1,10,-2};
BubbleSort(arr);
}
public static void BubbleSort(int[] arr){
//第一趟排序将最大的值放在倒数第一位
for(int j = 0;j<arr.length-1;j++){ //看完代码后试着把这里理解成arr.length-1-0
int temp = 0;//临时变量,进行交换元素时用到
if(arr[j]>arr[j+1]){
//如果前面的元素比后面元素大,则进行交换(从小到大排序)
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
System.out.println("第一趟排序后的结果:");
System.out.println(Arrays.toString(arr));
//第二趟排序将第二大的值放在倒数第二位
for (int j = 0;j< arr.length-1-1;j++){
int temp = 0;
if(arr[j]>arr[j+1]){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
System.out.println("第二趟排序后的结果:");
System.out.println(Arrays.toString(arr));
//第三趟排序将第三大的值放在倒数第三位
for (int j = 0;j< arr.length-1-2;j++){
int temp = 0;
if(arr[j]>arr[j+1]){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
System.out.println("第三趟排序后的结果:");
System.out.println(Arrays.toString(arr));
//第四趟排序将第四大的值放在倒数第四位
for (int j = 0;j< arr.length-1-3;j++){
int temp = 0;
if(arr[j]>arr[j+1]){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
System.out.println("第四趟排序后的结果:");
System.out.println(Arrays.toString(arr));
}
}
```
看这样的一段冒泡排序分解代码,想必你已经发现规律了,for循环体都是一样的,交换思想也是一样(使用临时变量存储值),那么我们就可以使用一个for循环将其整理为以下代码
ini
import java.util.Arrays;
public class MyBubbleSort {
public static void main(String[] args) {
int[] arr = {3,9,-1,10,-2};
BubbleSort(arr);
}
public static void BubbleSort(int[] arr){
//第一趟排序将最大的值放在倒数第一位
for(int i = 0;i< arr.length-1;i++){
for(int j = 0;j<arr.length-1-i;j++){
int temp = 0;//临时变量,进行交换元素时用到
if(arr[j]>arr[j+1]){
//如果前面的元素比后面元素大,则进行交换(从小到大排序)
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
System.out.println("第"+(i+1)+"趟排序后的结果:");
System.out.println(Arrays.toString(arr));
}
我们可以知道这种for循环嵌套for循环的时间复杂度是O(n^2),这样写的代码还不是最完美的,还可以进行优化,首先先看下面这一组数据的冒泡排序过程
从这组数据中我们可以发现,在第二趟交换了一次之后,数据已经变得有序了,那么接下来的操作都只是走"过程"而已
那么我们优化的思路就是,如果在某一趟排序中,没有发生任何一次的交换,那么就可以提前结束冒泡排序
ini
import java.util.Arrays;
public class MyBubbleSort {
public static void main(String[] args) {
int[] arr = {3,9,-1,10,20};
BubbleSort(arr);
}
public static void BubbleSort(int[] arr){
//第一趟排序将最大的值放在倒数第一位
for(int i = 0;i< arr.length-1;i++){
boolean flag = false;
for(int j = 0;j<arr.length-1-i;j++){
int temp = 0;//临时变量,进行交换元素时用到
if(arr[j]>arr[j+1]){
//如果进入这个条件说明发生了交换
flag = true;
//如果前面的元素比后面元素大,则进行交换(从小到大排序)
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
System.out.println("第"+(i+1)+"趟排序后的结果:");
System.out.println(Arrays.toString(arr));
if(!flag){
break;
}else {
flag = true; //如果有进行过交换,那么flag已经被改写为了true,我们需要重置,以备下一趟判断
}
}
这样我们就减少了一趟循环,也就优化成功了
快速排序
快速排序是对冒泡排序算法的一种改进,快速排序的基本思想是将要排序的数据分割成独立的两个部分,其中一部分的所有数据要比另外一部分的所有数据都要小,然后再按从方法对独立的两个部分进行快速排序,直到变得有序
接下来的的各种细节看代码演示,以及注解
scss
```
import java.util.Arrays;
public class MyQuickSort {
public static void main(String[] args) {
int[] arr = {-9,78,0,23,-567,70};
//定义每段快速排序的最左边的位置
int left = 0;
//定义每段快速排序最右边的位置
int right = arr.length-1;
QuickSort(arr,left,right);
System.out.println(Arrays.toString(arr));
}
public static void QuickSort(int[] arr,int l,int r){
if(l<r){
int pivotpos = partition(arr,l,r);
//将pivot左边部分进行快速排序
QuickSort(arr,l,pivotpos-1);
//将pivot右边部分进行快速排序
QuickSort(arr,pivotpos+1,r);
}
}
public static int partition(int[] arr,int l,int r){
int pivot = arr[l];
while (l<r){
//每次都要进行是否l<r的判断(因为极端的情况下比如右边都没有比pivot小的数,那么就会一直向前到不满足l<r)
while(l<r&&arr[r]>=pivot){
//若当前这个数不是比pivot小那么就进行向前找
r--;
}
//跳出循环说明右边找到小于pivot的数了
//那么就让其填入left的位置
arr[l] = arr[r];
while (l<r&&arr[l]<=pivot){
//若当前这个数不是比pivot大那么就进行向后找
l++;
}
//跳出循环说明找到了比pivot大的数
//那么就让将其填入right的位置
arr[r] = arr[l];
}
//这里也可以写arr[r] = pivot 因为跳出了大循环说明他们相遇了
arr[l] = pivot;
//放回当前相遇的位置,以便后续进行两部分的快速排序
return l;
}
}
```
选择排序
选择排序的基本思想很简单,就是在一组待排序数组arr中,第一次从arr[0]~arr[n-1]中寻找最小的元素,与arr[0]进行交换,第二次从arr[1]~arr[n-1]中寻找第二小的数与arr[1]进行交换,一直寻找当前待排序列的最小数进行交换直到有序为止
看着这张图进行选择排序的说明:
- 选择排序一共有数组大小-1轮排序
- 每一轮排序又是一个循环(目的是找到当前轮次最小值)
每一轮循环的规则如下:
- 先假定当前元素是最小元素,然后与每个元素进行比较,遇到比当前最小元素小的数,则重新设置其为最小元素,并且得到他的下标,当遍历完数组后就得到了最小数以及其下标,最后进行交换即可
接下来看选择排序的代码实现
ini
import java.util.Arrays;
public class MySelectSort {
public static void main(String[] args) {
int[] arr = {101,34,119,1};
SelectSort(arr);
}
public static void SelectSort(int[] arr){
//使用逐步推导的方式进行理解
//第一轮交换后结果 --> {1,34,119,101}
int minIndex = 0; //假定第一个数就是最小数,的下标
int min = arr[0]; //假定第一个数就是最小数,的数
for(int j = 0+1;j< arr.length;j++){
if(min>arr[j]){
//说明当前假定最小值并非最小值
min = arr[j]; //重置min
minIndex = j; //重置minIndex
}
}
//遍历完之后已经拿到最小值和最小值的下标,进行交换
if(minIndex!=0) { //如果minIndex还是为0,那么说明当前数就是最小值,那么就没有交换的必要了
arr[minIndex] = arr[0];
arr[0] = min;
}
System.out.println("第1轮的结果:");
System.out.println(Arrays.toString(arr));
//第二轮交换后结果 --> {1,34,119,101}
minIndex = 1; //假定第一个数就是最小数,的下标
min = arr[1]; //假定第一个数就是最小数,的数
for(int j = 1+1;j< arr.length;j++){
if(min>arr[j]){
//说明当前假定最小值并非最小值
min = arr[j]; //重置min
minIndex = j; //重置minIndex
}
}
//遍历完之后已经拿到最小值和最小值的下标,进行交换
if(minIndex!=1) {
arr[minIndex] = arr[1];
arr[1] = min;
}
System.out.println("第2轮的结果:");
System.out.println(Arrays.toString(arr));
//第三轮交换后结果 --> {1,34,101,119}
minIndex = 2; //假定第一个数就是最小数,的下标
min = arr[2]; //假定第一个数就是最小数,的数
for(int j = 2+1;j< arr.length;j++){
if(min>arr[j]){
//说明当前假定最小值并非最小值
min = arr[j]; //重置min
minIndex = j; //重置minIndex
}
}
//遍历完之后已经拿到最小值和最小值的下标,进行交换
if(minIndex!=2) {
arr[minIndex] = arr[2];
arr[2] = min;
}
System.out.println("第3轮的结果:");
System.out.println(Arrays.toString(arr));
}
}
通过上方的分解代码我们也找到了规律,那么现在用外层嵌套for循环来完成
ini
import java.util.Arrays;
public class MySelectSort {
public static void main(String[] args) {
int[] arr = {101, 34, 119, 1};
SelectSort(arr);
}
public static void SelectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i; //假定第一个数就是最小数,的下标
int min = arr[i]; //假定第一个数就是最小数,的数
for (int j = i + 1; j < arr.length; j++) {
if (min > arr[j]) {
//说明当前假定最小值并非最小值
min = arr[j]; //重置min
minIndex = j; //重置minIndex
}
}
//遍历完之后已经拿到最小值和最小值的下标,进行交换
if (minIndex != i) { //如果minIndex还是为0,那么说明当前数就是最小值,那么就没有交换的必要了
arr[minIndex] = arr[i];
arr[i] = min;
}
System.out.println("第" + (i + 1) + "轮的结果:");
System.out.println(Arrays.toString(arr));
}
}
}
归并排序
归并排序的基本思想是采用分治策略(将大问题分成小问题然后递归求解),归并排序的思想比较难理解,我们先看案例图解
分阶段可以理解为递归拆分子序列,而治阶段可以简单用一幅图介绍
接下来通过代码以及注释来理解归并排序
ini
import java.util.Arrays;
public class MyMergeSort {
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;
//先向左递归进行分解
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; //初始化i,左边有序序列的初识索引
int j = mid+1; //初始化j,右边有序序列的初识索引
int t = 0; //做为临时数组的索引
//1.先把左右两边的有序序列按照规则,填充到临时数组,直到有一方完全填充完为止
while (i<=mid && j<=right){
if(arr[i]<=arr[j]){
//左边的值比右边小,则填充左边的到临时数组,并且i++
temp[t] = arr[i];
i+=1;
t+=1; //临时数组的索引后移
}else {
//右边的值比左边小,则填充右边的到临时数组,并且j++
temp[t] = arr[j];
j+=1;
t+=1;
}
}
//2.把有剩余数据的一边依次全部填充到临时数组
while(i<=mid){
//说明左边没有填充完毕
temp[t] = arr[i];
i+=1;
t+=1;
}
while (j<=right){
//说明右边没有填充完毕
temp[t] = arr[j];
j+=1;
t+=1;
}
//3.将临时数组元素拷贝到原数组
//注意:这里不是每次都拷贝所有数据,比如进行递归的第一次合并,拷贝到原数组的元素只有0 1
t = 0;
int tempLeft = left; //并不是每次拷贝都是从头开始的,left拷贝的值是会发送变化的
while (tempLeft<=right){ //第一次合并tempLeft=0 right=1 / 第二次合并tempLeft=2 right=3......
arr[tempLeft] = temp[t];
tempLeft+=1;
t+=1;
}
}
}
这里需要注意的是理解递归的过程,合并并不是我们上方举例的最后一次合并,第一次合并只是合并两个元素,我们加上一些代码进行对合并内部的理解,结果为