排序算法
基本知识
时间复杂度
空间复杂度
稳定性:稳定性是针对当数组中存在多个相同的数时,如果改变原数组中多个相同的数的相对位置,则称该排序算法不稳定。
冒泡排序
冒泡排序对大家来说应该是非常不陌生吧,这个排序算法的思路也是十分的简单,主要是以趟作为单位,每趟通过相邻的两个数组元素比较选出一个最大(最小)的数放在数组的最后。(可能唯一需要注意的就是只需要进行数组元素数 - 1 趟即可完成排序)
这个因为实在太简单,直接上代码
package sort.test;
import java.util.Arrays;
public class TestBubble {
public static int[] arr = {4,10,22,5,6,1};
public static void sort(int[] arr) {
for(int i=1; i<arr.length; i++) {
for(int j=0; j<arr.length - i;j++) {
if(arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
public static void print() {
System.out.println(Arrays.toString(arr));
}
public static void main(String[] args) {
TestBubble.sort(TestBubble.arr);
TestBubble.print();
}
}
选择排序
这个和冒泡排序一样,也是属于非常简单的排序,主要思路和冒泡排序有点类似也有点出入,选择排序主要是记录最小值下标,在完成第一趟排序以后,将最小值与数组第一位进行交换。
这个也不详细讲解,上代码
package sort.test;
import java.util.Arrays;
public class TestSelect {
public static int[] arr = {4,10,22,5,6,1};
public static void sort() {
for(int i=0; i<arr.length - 1; i++) {
int min = i;
for(int j=i+1; j<arr.length;j++) {
if(arr[min] > arr[j]) {
min = j;
}
}
int temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
}
public static void print() {
System.out.println(Arrays.toString(arr));
}
public static void main(String[] args) {
TestSelect.sort();
TestSelect.print();
}
}
直接插入排序
直接插入排序就可以看出来他是将元素插入到对应位置,而不是通过交换的方式(与前两种差距就在这里)。它是通过跟前面的值进行对比,如果比自己大就放在自己的前面,比自己小就放在后面。
1.比自己大放在自己的后面这个很容易。
2.比自己小的时候放在自己的前面如何实现呢?这里需要使用到移位,或者说腾位置(腾空间),把这个值应该放置的合适的位置给他空出来(因为原来有人给他占着了)。
主要设计思想简单来说就是这些,具体解析在代码注解中
package sort;
import java.util.Arrays;
/**
* 直接插入排序
*
* 仔细说明:
* 该排序核心思想在于内定了两个不同的数组,分别为有序数组和无序数组
*
* 例如:{5,2,4,3}这个数组(从小到大排序
* 排序是通过比较插入位和插入位的上一次(甚至再往上面 n 位)
* 第一次排序是通过比较 2 和 5 的大小,显而易见需要交换。 交换后那么{2,5}就作为一个有序数组,而{4,3}作为一个无序数组进行下一阶段的排序
* 第二次排序是通过比较 5 和 4 的大小,显而易见需要交换。 交换后那么{2,4,5}就作为一个有序数组,{3}作为无序数组
* 关键:第三次排序通过比较 5 和 3 的大小,显而易见需要交换,但是交换的位置如果选择?
* --------------------这里是通过腾位置的方法实现的--------------------------
* 首先判断5 和 3,发现3需要在5前面,但是是不是需要在5的更前面不清楚,所以需要再次向前跟进
* 跟进到 4 和 3,发现3 需要在4前面,但是再前面依然未知,需要继续跟进
* 跟进到 2 和 3, 3发现了自己应该在的位置,应该在 2的后面,此时就需要讨论数组的整体后移问题让3正确的插入进来
* 所以我们凡是向前跟进一次,就将跟进前的上一位的值向后推一位
* 此时我们会面临一个问题,就是最初的那个值会被覆盖,所以我们需要一个变量对其保存,我们发现这个被覆盖的值就是插入点的值,即arr[i]
* 所以一切解决了
* 排序流程图:
*
* 第一次排序:{5,2,4,3} -> {5,5,4,3} -> {2,5,4,3} tips: insertValue保存了插入点的值 = 2
* 第二次排序:{2,5,4,3} -> {2,5,5,3} -> {2,4,5,3} tips: insertValue保存了插入点的值 = 4
* 第三次排序:{2,4,5,3} -> {2,4,5,5} -> {2,4,4,5} -> {2,3,4,5} tips: insertValue保存了插入点的值 = 3
*/
public class DirectInsert {
public static int[] sort(int[] arr) {
for (int i=1;i<arr.length;i++) {
int insertValue = arr[i];
//插入数组的值的前一个值的下标
int insertIndex = i-1;
//进行判断
//判断前一个值是否比自己大(从小到大排序)
//如果不必自己大,那么大可直接插在他的后面
//如果比自己大,那么则需要继续向前搜索寻找插入位置
while (insertIndex >= 0 && arr[insertIndex] > insertValue) {
/**
arr[insertIndex + 1] = arr[insertIndex]
这行代码相当于是不停地将该insertIndex值赋值给他的下一位
知道给当出现arr[insertIndex] < arr[i]时候给他腾空地
言简意赅说 : --------------腾--------------位----------------子--------------
细节:
这时候可能发现最初的那个arr[insertIndex + 1]的值被覆盖住了,但是最初的insertIndex + 1 = i
所以我们在一开始就定义了一个InsertValue来保存他的值
*/
arr[insertIndex + 1] = arr[insertIndex];
insertIndex--;
}
//出循环的时候说明已经找到合适位置插入indexValue
arr[insertIndex + 1] = insertValue;
}
return arr;
}
public static void print(int[] arr) {
System.out.println(Arrays.toString(arr));
}
public static void main(String[] args) {
int[] arr = {4,10,22,5,6,1};
DirectInsert.print(DirectInsert.sort(arr));
}
}
希尔排序
冒泡排序的进阶版-----希尔排序。
希尔排序是首先将数组进行分组操作,对分组的内部进行类似冒泡排序,当分组数等于1时候那么再执行一次操作后就排序结束。
可以认为希尔排序是多次的冒泡排序,但是为什么效率会更高呢,主要还是分组后,组内顺序已经排序好,当分组 = 1 时其实数组的顺序基本已经快排好,所以需要进行的排序操作其实不多。
分组:第一次分组(相当于初始化)是 group = length / 2,第二次则是group = group / 2。
如何决定哪些数组元素时一组,元素与数其下标 +(-) group 为一组。
例数组有10个元素。
第一次分组group = 5;
[0,5] [1,6] [2,7] [3,8] [4,9]
第二次分组group = 2;
[0,2,4,6,8] [1,3,5,7,9]
第三次分组group = 1;
[0,1,2,3,4,5,6,7,8,9]
希尔排序的思路就讲到这里,下面奉上代码
package sort;
import java.util.Arrays;
/**
* 希尔排序
* 说明
*
* 主要思想是通过:将数组进行分组,每次都分成长度的一半,10个数据分成5组 -> 2组 -> 1组
* 1.分组后,每个数组和他组内的数据进行排序,例如:10个数组分成5组后,1->6, 2->7, 3->8, 4->9, 5->10
* 当分组成2组时,每组有5个数据,(1,3,5,7,9),(2,4,6,8,10), 此时需要后序数据需要考虑和前面数据大小的比对
* (比对相当于冒泡排序,只能兼顾到两个数据的大小关系,所以需要后序数据兼顾前面数据)
* 2.最后分成一组的时候基本上内部顺序也不算太乱,所以只需要进行较少的排序即可完成排序任务。
*/
public class Shell {
/*
int[] arr = {4,10,22,5,6,1};
模拟希尔排序顺序:
第一次排序结果: {4,6,1,5,10,22}
第二次排序结果: {1,4,5,6,10,22}
*/
public static int[] sort(int[] arr) {
//首先确定组数 ,一开始为length的一半,之后每次减少一半
for (int group = arr.length/2; group>0; group = group/2) {
// i 代表一共有多少组,该层循环用于下层遍历所有的数据
for (int i = group; i<arr.length; i++) {
// 该层循环使用来处理组之间的顺序(对组内前面的数据进行比对)
for (int j = i - group; j>=0; j -= group) {
// 这里通过 j -= group 的方式向前推进,从而使得后面的数据可以比对前面的数据,进行数据比较
if (arr[j] > arr[j + group]) {
int temp = arr[j];
arr[j] = arr[j + group];
arr[j + group] = temp;
}
}
}
}
return arr;
}
public static void print(int[] arr) {
System.out.println(Arrays.toString(arr));
}
public static void main(String[] args) {
int[] arr = {4,10,22,5,6,1};
Shell.print(Shell.sort(arr));
}
}
快速排序
快速排序是一种递归实现的排序算法,主要是通过搜索的方法来进行的。
首先我们需要确定一个基准值(一般是数组的第一个元素),然后我们通过这个基准值,从数组的左右两侧进行搜索。
左侧:搜索大于基准值的下标,搜索到后停止。
右侧:搜索小于基准值的下标,搜索到后停止。
当左右侧搜索到相应的下标后,就将左右侧搜索到的下标对应的元素交换。当左侧下标大于右侧下标时(或者反过来),则代表搜索结束,需要将基准值对应下标与搜索终点进行交换(这样能使得数组左侧元素小于基准值,右侧元素大于基准值)
这样一次方法执行后虽然能够保证左侧小于基准,右侧大于基准,但是无法保证左右两侧内部顺序,所以需要对左右两侧进行递归。
主要思路就是上面的,下面奉上代码实现
package sort;
import java.util.Arrays;
/**
* 快速排序
* 说明
*
* 1.首先设定一个基准值(这里我设定的是数组的第一位)
* 2.以这个基准值作为标准,判断后序的数据和基准值的大小关系,如果比基准值小应该放在左边,比基准值大就放在右边
* 3.这个判断放在基准值左右的操作是以 left 和 right 两个指针进行的,left从左搜索大于基准值,right右搜索小于基准值的,
* 如果遇到后就退出循环,当两个都退出循环时候说明都遇到了各自的搜索对象,那么就将这两值进行交换,直到left >= right
* 4.当 left >= right 说明此时找不到不符的数,即已经分成基准值左右的两个数组,最后只需要将基准值从第一位替换到中间位即可
* (注意:需要先从右开始搜索)
* 5.虽然已经分成左数组(比基准值小)和右数组(比基准值大),但是左右数组的内部仍未有序,所以需要对左右数组进行递归操作
* 6.递归的出口应该是left >= right即分解成只有一个数据的数组
*/
public class Quick {
public static void sort(int[] arr, int left, int right) {
if(left >= right) {
return;
}
//指定一个基准目标
int target = arr[left];
int l = left;
int r = right;
//对数组的遍历(从小到大排序)
while (l < r) {
//此处是为了寻找一个小于target的数据下标
while (arr[r] >= target && l < r) {
r--;
}
//此处是为了寻找一个大于target的数据下标
while (arr[l] <= target && l < r) {
l++;
}
//如果成立说明在target两侧找到了放置不正确的数据的下标,直接将他们进行对调
if(l < r) {
int temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
}
}
//只要是跳出循环,就说明将数组的除开第一个数老说分成了两个部分
int temp = arr[left];
arr[left] = arr[l];
arr[l] = temp;
sort(arr,left,l - 1);
sort(arr,l + 1,right);
}
public static void print(int[] arr) {
System.out.println(Arrays.toString(arr));
}
public static void main(String[] args) {
int[] arr = {4,10,22,5,6,1};
Quick.sort(arr,0,arr.length-1);
Quick.print(arr);
}
}
归并排序
归并排序主要是通过分治法实现的。
通过将一个数组对半分开,一直分到只有一个元素的小数组时代表分部分结束,合并部分时就需要对两个单位(即分后的两个子数组)进行排序,这两个数组如何合并成一个数组呢?
这里就需要使用到一个中间数组temp进行一个过渡(所以说归并排序相比别的排序算法来说空间复杂度高一些),我们首先记录两个数组的下标,并且标记中间数组的下标,然后我们通过比较两个数组下标对应的元素,哪个元素小就放入temp中,并且将被选中数组的下标和中间数组temp向后移动一位,直到两个数组中一方元素放置完毕,一方放置完毕,那么势必另一方就仍有剩余元素,所以我们需要将剩余的元素放置到中间数组temp中。
当两个数组全部元素放置到temp中后,我们就需要将temp数组转移给原数组,则完成了排序。
(看完后可能有些疑问,当一方数组放置完毕后,另一方直接放入temp中,如何保证temp的剩余元素顺序?这个问题可能是这个分治法没有搞明白,因为我们分治法中在合并的时候就已经是有顺序的了,通俗点说就是两个氮元素数组合并成双元素数组时就会进行排序,只要合并就会进行排序,所以会保证两个数组的内部顺序的)
思路就讲到这里,下面奉上代码
package sort;
import java.util.Arrays;
/**
* 归并排序
* 说明
*
* 归并排序主要是通过使用分治的思想
* 1.首先将数组进行拆分,直到拆分成只有一个元素的数组后进行合并
* 2.合并时候通过两个指针的方式进行合并,并且使用到了临时存放数组temp(该临时数组是半有序数组)
*
* 合并过程:
* 1.有两个数组,左数组和有右数组
* 2.通过两个指针分别指向两个初始下标
* 3.通过循环比较对应下标的两个数组的值,判断谁较小些,将较小的数放入temp中,并将temp和较小数的数组的下标向后移动
* 4.循环结束时可能出现两种情况,左侧指针到头,和右侧指针到头,所以需要将左(右)数组的剩余数据放入temp中
* 5.将temp数组拷贝给arr
*
* 缺点:
* 经典的空间换时间的策略,使用额外的临时数组temp对数据进行一个保存
*/
public class Merge {
/**
* 对数组进行拆分
* @param arr 待排序数组
* @param left 数组左侧下标
* @param right 数组右侧下标
* @param temp 临时存放的数组
*/
public static void sort(int[] arr, int left, int right, int[] temp) {
if(left >= right)
return;
int mid = (right + left) / 2;
sort(arr,left,mid,temp);
sort(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 leftIndex = left;
int rightIndex = mid + 1;
int t = 0;
//循环判断左右数组对应指针下标的值的大小
while(leftIndex <= mid && rightIndex <= right) {
//此处说明左侧数组对应值小于右侧数组对应值,即左侧数组应该放入temp中
if(arr[leftIndex] <= arr[rightIndex]) {
temp[t] = arr[leftIndex];
leftIndex++;
}else {
//说明右侧数组对应值应该放入temp中
temp[t] = arr[rightIndex];
rightIndex++;
}
t++;
}
//对左侧数组中没有放入完的数据继续放入temp中
while(leftIndex <= mid) {
temp[t] = arr[leftIndex];
leftIndex++;
t++;
}
//对右侧数组中没有放入完的数据继续放入temp中
while(rightIndex <= right) {
temp[t] = arr[rightIndex];
rightIndex++;
t++;
}
//最后将temp数组拷贝给arr数组
int l = left;
t = 0;
while(l <= right) {
arr[l] = temp[t];
t++;
l++;
}
}
public static void print(int[] arr) {
System.out.println(Arrays.toString(arr));
}
public static void main(String[] args) {
int[] arr = {4,10,22,5,6,1};
int[] temp = new int[arr.length];
Merge.sort(arr,0,arr.length-1,temp);
Merge.print(arr);
}
}
基数排序
这是一种非常特殊的排序方式,他是针对于非常多的数,并且数的分布或者说是数之间的间距不算太大时使用的一种排序算法。
主要思想是:选出数组中最大的数,创建 length = 最大数 + 1的一个数组target,然后遍历待排序数组,将元素值最为target数组的下标,每存放一个就将其下标 + 1,这样就完成了排序。(target下标为真正的元素,target的元素为出现的次数,所以只要根据出现次数输出那么多次即可。)
所以我们可以看到,基数排序有点类似于计数的方式进行排序。
因为十分简单,所以我直接奉上代码。
package sort;
public class Cardinal {
public static int[] arr = {2,1,3,4,2,3,1,3,4,5,7,6,4,2,1};
public static int[] sort(int[] arr) {
int max = 0;
for(int i=0; i<arr.length; i++)
max = Math.max(arr[i],max);
int[] target = new int[max + 1];
for(int i=0; i<arr.length; i++) {
target[arr[i]]++;
}
return target;
}
public static void print(int[] arr) {
for(int i=0; i<arr.length; i++) {
if(arr[i] > 0) {
for(int j=0; j<arr[i]; j++) {
System.out.print(i + " ");
}
}
}
}
public static void main(String[] args) {
int[] target = Cardinal.sort(Cardinal.arr);
Cardinal.print(target);
}
}
堆排序
必知
在了解堆排序之前我们需要了解堆的概念(这里说的不是JVM的那个堆),堆分为小顶堆和大顶堆两种。
小顶堆:小顶堆是指父节点永远小于等于两孩子结点(但是两孩子结点无任何关系)
大顶堆:大顶堆是指父节点永远大于等于两孩子结点(但是两孩子结点无任何关系)
TIPS:这里需要了解一个知识点,我们在构建堆这个结构时一般使用的都是顺序存储,并且堆是一个完全二叉树。
完全二叉树顺序存储具有的性质:
1.当根结点索引为0时:左孩子:2i + 1,右孩子:2i + 2(i为父节点索引)
2.当根结点索引为1时:左孩子:2i,右孩子:2i + 1(i为父节点索引)
3.父节点的索引:(i - 1)/ 2(i为孩子结点索引)
基本知识我们普及完了,下面讲一下堆排序的实现思想。
设计思想
堆排序他是通过将数组构造成一个大顶堆(小顶堆),通过获取此时堆的极值(即最大值或最小值,也是堆顶)放入数组对应位置后,将数组最后一位直接覆盖堆顶值(也可以发现是数组首位index = 0),然后将数组最后一位删除。
此时我们知道将最后一位放到堆顶后就需要重新构建这个堆,如何构建呢?
我是通过向下递归他的孩子结点完成重构的。(这部分在堆的delete()方法中 需要实现的,因为该博客主要将排序算法,堆的实现估计也可以写一篇博客了,如果有需要堆实现的代码,可以私信问我要)
周而复始的取出堆的极值 -> 重构堆 -> 取出堆的极值,直到将堆的元素全部取出就完成了排序。
下面奉上代码(代码中也有详细注解。需要注意的是代码中的MaxHeap是我自己的写的大顶堆)
package sort;
import heap.MaxHeap;
import java.util.Arrays;
public class HeapSort {
public static int[] sort(int[] arr) {
MaxHeap maxHeap = new MaxHeap();
int length = arr.length;
//首先构造一个大顶堆
for(int i : arr) {
maxHeap.insert(i);
}
for(int index=length-1; index>=0; index--) {
//将最大值取出
int max = maxHeap.get(0);
//删除最大值(我这里的MaxHeap的delete方式就是使用的将数组最后一位取出放入被删除位置并且重构大顶堆)
//所以可以省略删除、重构大顶堆部分的内容
maxHeap.delete(max);
//将最大值放置到数组最后一位
arr[index] = max;
//交换后重构大顶堆(因为此时原大顶堆最大值位置被数组最后一位替换,所以需要向下递归重构大顶堆)
//maxHeap.downSwap(maxHeap.get(0),0);
}
return arr;
}
public static void print(int[] arr) {
System.out.println(Arrays.toString(arr));
}
public static void main(String[] args) {
int[] arr = {4,10,22,5,6,1};
HeapSort.sort(arr);
HeapSort.print(arr);
}
}