1.插入排序
插入排序的基本思想就是将待排序的数据,根据这个待排数据的大小插入一个已经排序好的序列中,直到将数组中的所有数据都插入完为止,得到一个新的有序序列
1.1直接插入排序
直接插入排序就是一种简单的插入排序。

直接插入排序就是将第一个数据当成已经是有序序列了,从第二个数据开始遍历,假设每次开始遍历的数据的下标为i,然后创建一个tmp变量来存储arr[i],然后从i-1位置开始往前遍历,用变量j来记录从i-1位置开始往前遍历时遇到的下标。
记住一个要点:i位置前面一定是一个有序序列
此时会有两种情况:
第一种情况:如果arr[j]>tmp,此时让arr[j+1]=arr[j]即可,然后让j--

第二种情况:如果arr[j]<=tmp,此时让arr[j+1]=tmp,此时跳出循环即可

此时还需要考虑到一种j越界的情况,如下图

代码实现:
java
public class Sort {
/**
* 时间复杂度:O(N^2)
* 空间复杂度:O(1)
* 稳定性:稳定
* @param arr
*/
public static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int tmp = arr[i];
int j=i-1;
for(;j>=0;j--){
if (arr[j]>tmp){
arr[j+1]=arr[j];
} else {
arr[j+1]=tmp;
break;
}
}
arr[j+1]=tmp;
}
}
}
测试:

直接插入排序有一个特点:数组越有序,排序效率就越高
1.2希尔排序
希尔排序也叫缩小增量排序,希尔排序就是在排序前对数组进行分组,有一个gap(两个数据之间的距离)的概念,将gap相同的数据分为一组,然后对每一组数据进行排序即可。
当gap==1时,在统一将数据进行排序即可

此时通过上图可以发现,一组中的数据如果越少,就越无序,但此时因为数据少,反而又会快了一点。如果数据越多,就越接近有序,但是又因为数据多,反而会慢了一点点
如当gap==1时,此时所有数据就为一组了,此时反而是更有序的
如何对一组数据进行排序呢?其实就是我们对每一组数据进行直接插入排序即可
其实希尔排序可以看做是对直接插入排序的优化,因为希尔排序会让数组越来越有序,越有序,直接插入排序就越快,
代码实现:
java
public class Sort {
public static void shellSort(int[] arr) {
int gap = arr.length;
while (gap>1){
gap/=2;
shell(arr,gap);
}
}
private static void shell(int[] arr, int gap) {
for (int i = gap; i < arr.length; i++) {
int tmp = arr[i];
int j=i-gap;
for(;j>=0;j-=gap){
if (arr[j]>tmp){
arr[j+gap]=arr[j];
} else {
arr[j+gap]=tmp;
break;
}
}
arr[j+gap]=tmp;
}
}
}
测试:

2.选择排序
2.1直接选择排序
直接选择排序就是每次排序一个数字时,假设这个数字的下标为i,创建一个变量minIndex来记录这次排序时最小数字的下标,先将minIndex初始化为i,创建变量j,然后j从i+1的位置完后遍历,如果遇到比arr[minIndex]小的数字,则更新minIdex,当j遍历完一次数组,此时minIndex就记录这次排序时的最小数字的下标,然后交换arr[i]和arr[minIndex]的值即可
代码实现:
java
public class Sort {
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int minIndex = i;
for (int j=i+1;j< arr.length;j++){
if (arr[j]<arr[minIndex]){
minIndex=j;
}
}
swap(arr,i,minIndex);
}
}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
}
测试:

2.2直接选择排序优化
此时可以定义一个变量left=0和一个变量right=arr.length-1,此时创建两个变量,minIndex和manIndex,minIndex用来记录这次排序时最小数字的下标,maxIndex用来记录这次排序时的最大数字的下标。
此时还是从left+1的位置开始遍历,如果遇到arr[j]<arr[minIndex],此时更新minIndex,如果遇到arr[i]>arr[maxIndex],此时更新maxIndex。
当一次排序完之后,就交换arr[left]和arr[minIndex],交换arr[right]和arr[maxIndex]
交换完之后,让left++,right--,知道left和right相遇,此时就排序完成了。
代码实现:
java
public class Sort {
public static void selectSort2(int[] arr){
int left=0;
int right=arr.length-1;
while(left<right){
int minIndex = left;
int maxIndex = left;
for (int i = left+1; i <= right ; i++) {
if (arr[i]<arr[minIndex]){
minIndex=i;
}
if (arr[i]>arr[maxIndex]){
maxIndex=i;
}
}
swap(arr,left,minIndex);
swap(arr,right,maxIndex);
left++;
right--;
}
}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
}
此时运行代码时,排序会出错,如下图

因此此时有一个特殊情况,此时可能会出现一个maxIndex==left的情况,什么意思呢?

代码中是先交换arr[left],arr[minIndex],如下图,此时这次下次交换是没有问题的

此时在交换arr[right]和arr[maxIndex]就会出现问题了,因为一开始arr[maxIndex]是等于12的,而在上次交换时,就将arr[maxIndex]变为2,本来是想将12与arr[right]交换的,此时却变成了2与12交换,所以此时就出错了
此时只需要对left==maxIndex这种特殊情况进行处理即可
java
public class Sort {
public static void selectSort2(int[] arr){
int left=0;
int right=arr.length-1;
while(left<right){
int minIndex = left;
int maxIndex = left;
for (int i = left+1; i <= right ; i++) {
if (arr[i]<arr[minIndex]){
minIndex=i;
}
if (arr[i]>arr[maxIndex]){
maxIndex=i;
}
}
swap(arr,left,minIndex);
if(left==maxIndex){
maxIndex=minIndex;
}
swap(arr,right,maxIndex);
left++;
right--;
}
}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
}

2.3堆排序
堆排序,如果想进行升序排序的话,先创建一个大根堆,创建一个堆,首先就要找到最后一棵子树,从最后一棵树开始调整为大根堆,每调整一棵树时,都要向下调整,前面的一篇文章中讲过这个问题
堆排序的话,如果是升序,首先找出最后一个节点的下标,假设为end,然后交换arr[0]和arr[end],然后从将第一棵树调整为大根堆,重复此操作,知道end<=0
代码实现:
java
public class Sort {
public static void heapSort(int[] arr){
createHeap(arr);
int end = arr.length-1;
while (end>0){
swap(arr,0,end);
shiftDown(arr,0,end);
end--;
}
}
private static void createHeap(int[] arr) {
int last = arr.length-1;
for (int parent = (last-1)/2;parent>=0;parent--){
shiftDown(arr,parent,arr.length);
}
}
private static void shiftDown(int[] arr, int parent, int length) {
int child = 2*parent+1;
while (child<length){
if (child+1<length&&arr[child+1]>arr[child]){
child=child+1;
}
if (arr[child]>arr[parent]){
swap(arr,parent,child);
parent=child;
child=parent*2+1;
} else {
break;
}
}
}
}
3.交换排序
3.1冒泡排序
冒泡排序记住两个要点:第一个循环是比较的趟数,第二个循环是每一趟比较的次数。
代码实现:
java
public static void bubbleSort(int[] arr){
for (int i=0;i< arr.length-1;i++){
boolean flag = true;
for (int j=0;j< arr.length-1-i;j++){
if (arr[j+1]<arr[j]){
swap(arr,j,j+1);
flag=false;
}
}
if (flag){
break;
}
}
}
private static void swap(int[] arr, int i, int j) {
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
3.2快速排序
1.Hoare版快速排序
我们以升序排序为例,首先要找到一个基准值,定义两个指针left和right,然后将arr[left]作为基准值,然后我们就先从right指针开始往前找比基准值的小的,每次right指针找到比基准值小的,right指针就停下来,然后再从left指针开始往后走找比基准值大的,当left指针走到比基准值大,此时交换left和right指向的值,然后重复此操作,知道left和right相遇,最终在将left或者right指向的值与基准值交换,如下图

此时一次排序后数组会有一个特点,我们会用一个变量mid来记录left和right相遇的位置,mid左边的数字全是比arr[mid]小的,mid右边的数字全是比arr[mid]大的数字,如下图

然后此时就可以对mid的左边在进行同样的排序,左边进行完同样的排序后,在对mid右边的数字进行同样的排序,如下图,我只画了左边那部分,右边那部分同理

所以此时就可以通过递归左右子数的形式去处理左右部分的排序。
递归的时候要注意返回的条件,除了left==right这个条件外,还有left>right这个条件,如下图

代码实现:
java
public class Sort {
private static void quick(int[] arr, int start, int end) {
if (start>=end){
return;
}
int mid = findMid(arr,start,end);
quick(arr,start,mid-1);
quick(arr,mid+1,end);
}
private static int findMid(int[] arr, int left, int right) {
int tmp = arr[left];
int tmpLeft=left;
while (left<right){
while (left<right&&arr[right]>=tmp){
right--;
}
while (left<right&&arr[left]<=tmp){
left++;
}
swap(arr, left, right);
}
swap(arr,left,tmpLeft);
return left;
}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
}
1.为什么不先从从left开始往后找比基准值大的呢?
在我的理解中,快速排序的核心就是首先要让整体有序,什么意思呢?就以上面的例子为例,mid的前面的数字都是比arr[mid]小的数字,而mid后面的数字都是比arr[mid]大的数字,整体有序就是这种意思。
则此时如果我们先让left往后找比基准值大的,再从right开始往前开始找比基准值小的,此时就会有一个问题,假设之前arr[left]和arr[right]发生了一次交换,也就是说,此时arr[right]会是一个比基准值大的数字了,此时如果先让left先走,此时left可能会直接跑到right的位置,此时直接与right相遇,left和right相遇,此时就要将arr[right]和基准值交换,此时是不敢交换的,因为快速排序要讲究一个整体有序,此时如果arr[right]和基准值发生了交换,那么此时mid的前面部分就会存在一个比arr[mid]大的数字,这个就不符合整体有序的这部分。
如下图

2.移动的条件中能将等号去掉吗?

因为此时如果没有等号,遇到下面的这种情况,会导致死循环,如下图
此时的基准值为6,此时如果arr[left]和arr[right]都等于6,如果没有等号,left和right不会移动,此时arr[left]和arr[right]发生交换,此时交不交换都一样,没有等号,即使交换了,left和right是一定不会动的,所以此时就会导致死循环。

2.挖坑法实现快速排序
挖坑法就是抽象点来说就是先将基准值存储到一个变量中,然后还是从right开始,往前找比基准值小的,此时如果遇到比基准值小的,此时直接将arr[left]=right,接着再从left开始往后找比基准值大的,此时如果遇到比基准值大的,此时直接让arr[right]=arr[left],最终left和right相遇,此时让arr[left]=tmp即可
如何理解呢?
我们只需要记住一点,将right遇到比基准值小的值放到前面,将left遇到的比基准值大的值放到后面。
代码实现:
java
public class Sort {
private static void quick(int[] arr, int start, int end) {
if (start>=end){
return;
}
int mid = findMid(arr,start,end);
quick(arr,start,mid-1);
quick(arr,mid+1,end);
}
public static int partition(int[] arr,int left,int right){
int tmp = arr[left];
while (left<right){
while (left<right&&arr[right]>=tmp){
right--;
}
arr[left]=arr[right];
while (left<right&&arr[left]<=tmp){
left++;
}
arr[right]=arr[left];
}
arr[left]=tmp;
return left;
}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
}
3.前后指针法快速排序
定义一个指针prev=left,cur=left+1,当遇到(arr[cur]<tmp&&arr[++prev]!=arr[cur])时,才交换arr[cur]和arr[prev],否则让cur++,最终cur>right跳出循环时,交换arr[left]和arr[prev]
代码实现:
java
public class Sort {
private static void quick(int[] arr, int start, int end) {
if (start>=end){
return;
}
int mid = findMid(arr,start,end);
quick(arr,start,mid-1);
quick(arr,mid+1,end);
}
public static int partition(int[] arr,int left,int right){
int tmp=arr[left];
int prev=left;
int cur=left+1;
while (cur<=right){
if (arr[cur]<tmp&&arr[++prev]!=arr[cur]){
swap(arr,prev,cur);
}
cur++;
}
swap(arr,left,prev);
return prev;
}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
}
4.一个小总结
不管是哪一个版本的快速排序,最关键的核心就是求mid那块,不管是什么版本的快速排序,partition方法的主要功能就是让mid前面的数字都小于arr[mid],让mid后面的数字大于arr[mid]。
5.快速排序优化
快速排序有一个缺点,就是在数组有序或者逆序的情况下,递归的情况会退化成一棵单边树,这就会导致快速排序的时间复杂度达到O(N^2)
5.1优化方案一:三数取中法
为什么在有序和逆序的情况下,快速排序的时间复杂度会达到O(N^2),因为这两种情况left和right第一次相遇的位置要么是在0下标或者最后一个位置,此时就会导致递归退化成单边树
三数取中法的核心思想就是打破那种有序性,从当前区间的左端,中间和右端三个元素中选取中间值作为pivot,尽可能让left和right不在最左边和最右边相遇,通过交换tmp和arr[mid]来破坏有序性,让left的后面有一个比tmp小的,能够让right在寻找比tmp小的停下来,此时pivot两边都会有数据,避免形成单边树。
快速排序的性能依赖于pivot的选择。当数组中的数据为有序或者无序时,此时每次递归都会将第一个或者最后一个位置作为pivot,那么分区后会出现一边没有数据,另一边数据有n-1个数据,这就会导致递归树退化成单边树,递归深度为O(N),时间复杂度会达到O(N^2)

代码实现:
java
package sort2;
public class Sort {
public static void quickSort(int[] arr){
quick(arr,0,arr.length-1);
}
private static void quick(int[] arr, int start, int end) {
if (start>=end){
return;
}
int mid=findMid(arr,start,end);
swap(arr,start,mid);
System.out.println("start: "+start+" end: "+end);
int pivot = partition(arr,start,end);
quick(arr,start,pivot-1);
quick(arr,pivot+1,end);
}
/**
* 三数取中
* @param arr
* @param left
* @param right
* @return
*/
private static int findMid(int[] arr, int left, int right) {
int mid=(left+right)/2;
if (arr[left]<arr[right]){
if (arr[mid]>arr[right]){
return right;
}else if(arr[mid]<arr[left]){
return left;
}else {
return mid;
}
}else {
if (arr[mid]>arr[left]){
return left;
}else if (arr[mid]<arr[right]){
return right;
}else {
return mid;
}
}
}
public static int partition(int[] arr,int left,int right){
int tmp=arr[left];
while (left<right){
while (left<right&&arr[right]>=tmp){
right--;
}
arr[left]=arr[right];
while (left<right&&arr[left]<=tmp){
left++;
}
arr[right]=arr[left];
}
arr[left]=tmp;
return left;
}
private static int partitionHoare(int[] arr, int left, int right) {
int tmp=arr[left];
int tmpLeft=left;
while (left<right){
while (left<right&&arr[right]>=tmp){
right--;
}
while (left<right&&arr[left]<=tmp){
left++;
}
swap(arr,left,right);
}
swap(arr,left,tmpLeft);
return left;
}
private static void swap(int[] arr, int i, int j) {
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
}
5.2优化方案二:减少递归的次数
首先要知道一个点,快排的特点就像一颗二叉树,越往下的节点就越多,节点越多就意味着快排的递归的次数就越多,递归的次数变得多就有可能导致栈溢出。
但是在快排中,还有一个特点,就是每次向下递归,排序的区间就会越小,越小就意味着这段区间的数字越有序,此时就可以通过插入排序来优化,因为插入排序的特点是数组越有序,排序的速度就越快
代码实现:
java
public static void quickSort(int[] arr){
quick(arr,0,arr.length-1);
}
private static void quick(int[] arr, int start, int end) {
if (start>=end){
return;
}
if (start-end+1<3){
insertSortRange(arr,start,end);
}
int mid=findMid(arr,start,end);
swap(arr,start,mid);
// System.out.println("start: "+start+" end: "+end);
int pivot = partition(arr,start,end);
quick(arr,start,pivot-1);
quick(arr,pivot+1,end);
}
private static void insertSortRange(int[] arr, int start, int end) {
for (int i = start; i <= end; i++) {
int tmp=arr[i];
int j=i-1;
for (;j>=start;j--){
if (arr[j]>tmp){
arr[j+1]=arr[j];
}else {
arr[j+1]=tmp;
break;
}
}
arr[j+1]=tmp;
}
}
/**
* 三数取中
* @param arr
* @param left
* @param right
* @return
*/
private static int findMid(int[] arr, int left, int right) {
int mid=(left+right)/2;
if (arr[left]<arr[right]){
if (arr[mid]>arr[right]){
return right;
}else if(arr[mid]<arr[left]){
return left;
}else {
return mid;
}
}else {
if (arr[mid]>arr[left]){
return left;
}else if (arr[mid]<arr[right]){
return right;
}else {
return mid;
}
}
}
public static int partition(int[] arr,int left,int right){
int tmp=arr[left];
while (left<right){
while (left<right&&arr[right]>=tmp){
right--;
}
arr[left]=arr[right];
while (left<right&&arr[left]<=tmp){
left++;
}
arr[right]=arr[left];
}
arr[left]=tmp;
return left;
}
6.快速排序的非递归实现
快速排序的非递归实现可以通过栈或者队列实现的,下面,我将以栈为例子来讲解
其实无论是递归还是非递归来实现快速排序,核心就是能够正确找到待排序区间的left和right,此时就可以利用栈来记录每次排序区间的left和right。
此时有一个小细节:因为我们是通过pivot来寻找left和right,但是如果pivot的某一边只有一个元素的话,此时因为这段待排序区间只有一个元素,此时就不用记录这段待排序区间的left和right了,因为一个元素的区间就是有序的

代码实现:
java
public static void quickSort(int[] arr){
quickNor(arr,0,arr.length-1);
}
private static void quickNor(int[] arr, int start, int end){
int pivot=partition(arr,start,end);
Stack<Integer> stack=new Stack<>();
if (pivot>start+1){
stack.push(start);
stack.push(pivot-1);
}
if (pivot<end-1){
stack.push(pivot+1);
stack.push(end);
}
while (!stack.isEmpty()){
end=stack.pop();
start=stack.pop();
pivot=partition(arr,start,end);
if (pivot>start+1){
stack.push(start);
stack.push(pivot-1);
}
if (pivot<end-1){
stack.push(pivot+1);
stack.push(end);
}
}
}
4.归并排序
归并排序的核心思想:
首先递归得把数组不断地从中间分成两半(分区),知道每个子数组只有一个元素,此时只有一个元素的子数组是天然有序的,然后,再把这些有序的子数组,两两合并成一个更大的有序数组,知道最终合并成一个完整的,有序的数组,合并的过程是归并排序的关键,保证了两个有序数组合并后依然有序.
4.1递归实现
归并排序的操作分为两步:分解和合并,如下图

我的理解:虽说是有分解的过程,但是并不是真正的将数组拆了,而是一种抽象的拆,这种拆就是通过下标划分区域,其实这里的"拆"是指划分区域,比如[left,mid]区间就是一个区域,[mid+1,right]区间又是一个区域,这两个区域看起来是分开了,但是这两部分还是在同一个数组里面且这两个区域是相连的,但是其实这两个区域是在同一个数组中连续的两块区域,如下图

此时在写合并的代码,有几个细节需要注意:
第一个细节:在每次合并时,我们会创建一个中间数组,先存储好排序的结果,这个中间数组的大小时right-left-1
第二个细节:当中间数组存储好排序的结果后,此时要将中间数组的排序结果复制到原来的数组里面,此时复制的时候要写成arr[i+left]=tmp[i],不能写成arr[i]=tmp[i],因为归并是一段区间一段区间来进行排序的,这段排序区间的起点不一定是0,但区间的的起点一定是left,由于tmp是从0开始的,所以此时要arr[i+left]可以对应到tmp[i]

第三个细节:下面这种写法也是错的,因为tmp是要从0开始遍历的,此时left不一定是0

完整代码实现:
java
public class Sort {
public static void mergeSort(int[] arr){
mergeTmp(arr,0,arr.length-1);
}
private static void mergeTmp(int[] arr, int left, int right) {
if (left==right){
return;
}
//分解
int mid=(left+right)/2;
mergeTmp(arr,left,mid);
mergeTmp(arr,mid+1,right);
//合并
merge(arr,left,mid,right);
}
private static void merge(int[] arr, int left, int mid, int right) {
int s1=left;
int e1=mid;
int s2=mid+1;
int e2=right;
int k=0;
int[] tmp=new int[right-left+1];
while (s1<=e1 && s2<=e2){
if (arr[s1]<=arr[s2]){
tmp[k++]=arr[s1++];
}else{
tmp[k++]=arr[s2++];
}
}
while (s1<=e1){
tmp[k++]=arr[s1++];
}
while (s2<=e2){
tmp[k++]=arr[s2++];
}
for (int i = left; i < k; i++) {
arr[i]=tmp[i];
}
}
}
4.2 非递归实现
这里的非递归实现,本质是用迭代的方式替代了递归方式中的分区(拆分过程).
在递归版本的归并排序中,通过不断得递归,将数组划分为更下的数组,知道每个子数组只包含一个元素(此时的数组是天然有序的),然后在依次合并这些有序的子数组,最终使整个数组有序
而在非递归的实现中,直接从最小的,长度为1的有序子数组开始,此时就可以直接开始合并了,每一个依次合并,就将子数组的长度扩大(假设原来的子数组的长度为len,扩大后的长度为len=len*2),并继续按照新的长度来划分数组和合并,知道子数组的长度超过或者等于原数组的长度,此时整个数组已经变为一个完全有序的数组
总之就是按照我们上面分析的子数组的长度的变换(gap=1,2,4,8......&&len<arr.length),每次都已len来将数组分成多个子数组,然后找出每个子数组对应的left,midright边界,将相邻的两个有序子数合并成一个更大的有序子数组(红色这步跟递归的合并方式一样,直接调用即可)

代码实现:
java
public class Sort {
public static void mergeSort(int[] arr){
mergeTmp(arr,0,arr.length-1);
}
public static void mergeNor(int[] arr){
int gap=1;
while (gap<arr.length){
for (int i = 0; i < arr.length; i++) {
int left=i;
int mid=left+gap-1;
//处理mid越界的情况
if (mid>=arr.length){
mid=arr.length-1;
}
int right=mid+gap;
//除了right越界的情况
if (right>=arr.length){
right=arr.length-1;
}
merge(arr,left,mid,right);
}
gap*=2;
}
}
private static void merge(int[] arr, int left, int mid, int right) {
int s1=left;
int e1=mid;
int s2=mid+1;
int e2=right;
int k=0;
int[] tmp=new int[right-left+1];
while (s1<=e1 && s2<=e2){
if (arr[s1]<=arr[s2]){
tmp[k++]=arr[s1++];
}else{
tmp[k++]=arr[s2++];
}
}
while (s1<=e1){
tmp[k++]=arr[s1++];
}
while (s2<=e2){
tmp[k++]=arr[s2++];
}
for (int i = left; i < k; i++) {
arr[i]=tmp[i];
}
}
}