排序算法自我总结
排序:使得序列成了按照关键词有序的序列的过程。
排序的种类:
1.稳定性排序和不稳定排序:主关键词排序,是否会改变原有的顺序。
2.内排序和外排序:内排序,待排序的记录均会存入内存中。外排序则部分存入。
3.简单排序和改进排序:简单排序有:冒泡排序,选择排序和插入排序。
1. 冒泡排序(从小到大)
1.1 基本原理
两两比较相邻记录的关键词,如果反序则交换,直到没有反序为止(常常从后向前比较)
1.2 过程步骤
1)比较相邻的两个数据,如果第二个数更小,则交换
2)从后向前两两比较数据大小,一直到第一个数,使得最小值到第一个位置处。
3)重复上述过程,使得第二个数和第三个最小排好位置
1.3 代码实现
public class demo1 {
public static void main(String[] args) {
//冒泡排序
int[] list=new int[]{6,5,4,3,2,1};
maopao(list);
}
//方法:冒泡
public static void maopao(int [] list){
int temp;//交换临时变量
for(int i=0;i<list.length;i++){
for(int j= list.length-2; j>=i;j--){ //从倒数第二个开始
if(list[j]>list[j+1]){
temp=list[j+1];
list[j+1]=list[j];
list[j]=temp;
}
}
}
System.out.println("list="+ Arrays.toString(list));
//list=[1, 2, 3, 4, 5, 6]
}
}
1.4 适用场景
- 平均时间复杂度为:O(n2) ; 空间复杂度为:O(1) ;稳定排序。
- 当大小满足前大于后时,不会改变顺序,是稳定排序;
- 时间复杂度较高,不适合海量数据
1.5 代码优化
1)冒泡排序按之前流程,不会中途结束,会比较完所有的数,故可以设置一个变量,如果进行一轮比较,没有交换顺序,则视为已经排好顺序了。
2)修改后:算法在数据基本有序的前提下,最好的时间复杂度为O(N),其他情况为O(N2)。
public class demo1 {
public static void main(String[] args) {
//冒泡排序
int[] list=new int[]{1,2,4,3,5,6};
maopao(list);
}
//方法:冒泡
public static void maopao(int [] list){
int temp;//交换临时变量
for(int i=0;i<list.length;i++){
boolean flag=false;
for(int j= list.length-2; j>=i;j--){ //从倒数第二个开始
if(list[j]>list[j+1]){
temp=list[j+1];
list[j+1]=list[j];
list[j]=temp;
flag=true;
}
}
if(flag==false) break;
}
System.out.println("list="+ Arrays.toString(list));
}
}
2. 选择排序
2.1 基本原理
在未排序序列中找到最小(大)元素,交换存放到排序序列的起始位置。
2.2 实现步骤
第一次遍历n-1个数,然后选出最小的数和第一个数进行交换
第二次遍历n-2个数,先出最小的数和第二个数进行交换
…
第 i 次遍历n-1-i个数,先出最小的数和第 i 个数进行交换
2.3 代码实现
public class 简单选择排序 {
public static void main(String[] args) {
//简单选择
int[] list=new int[]{3,5,6,1,2,4};
xuanze(list);
}
public static void xuanze(int[] list){
//临时变量
int temp=0;
int minindex;
for(int i=0;i<list.length;i++){
//1)找出最小
minindex=i;
for(int j=i;j<list.length;j++){
if(list[j]<list[minindex]) minindex=j;
}
System.out.println("minindex="+minindex);
//2)进行交换(i与minindex的数据)
temp=list[i];
list[i]=list[minindex];
list[minindex]=temp;
}
System.out.println("List="+Arrays.toString(list)); //List=[1, 2, 3, 4, 5, 6]
}
}
2.4 适用场景
- 平均时间复杂度为:O(n2) ; 空间复杂度为:O(1) ;不稳定排序。
- 使用于小数据进行排序。
3. 插入排序
3.1 基本原理
简单而高效的算法。将一个记录插入到已经排好序的有序表中,得到一个新的,记录数加一的有序表
3.2 过程步骤
- 把待排序的数分为已排序和未排序两部分。第一个数默认已排序
- 从第二个数开始,向前比较,在已经排好序的数中找到合适插入第二个数的位置,并不停交换顺序。
- 重复上述过程,直到最后一个数被排好序。
3.3 代码实现
public class 直接插入排序 {
public static void main(String[] args) {
//直接插入测试用例
int[] list=new int[]{3,5,6,1,2,4,8,9,7};
method(list); //list=[1, 2, 3, 4, 5, 6, 7, 8, 9]
}
//直接插入排序实现方法
public static void method(int[] list){
for(int i=0;i<list.length;i++){
int index=i;
int temp=0;
//从后向前比较
while(index>=1 && list[index-1]>list[index]){
//交换位置
temp=list[index-1];
list[index-1]=list[index];
list[index]=temp;
//
index--;
}
}
System.out.println("list="+ Arrays.toString(list));
//list=[1, 2, 3, 4, 5, 6, 7, 8, 9]
}
}
方法二:
public static void method(int[] list){
int temp;
for(int i=1;i<list.length;i++){
for(int j=i;j>=1;j--){
//后小于前,交换
if(list[j-1]>list[j]){
temp=list[j-1];
list[j-1]=list[j];
list[j]=temp;
}else{
break;
}
}
}
System.out.println("list="+ Arrays.toString(list));
}
方法三(最短):
public static void method2(int[] list){
//循环一:需要插入元素的位置遍历
for(int i=1;i<list.length;i++){ //需要插入的数
int temp=list[i]; //需要插入的数
int j=i-1;//指向插入前面的一个元素
//循环二的作用:从后向前比较,如果大于插入值,就先后移动
for(j=i-1; j>=0 &&list[j]>temp; j=j-1){
list[j+1]=list[j];
}
list[j+1]=temp;
}
System.out.println("list="+ Arrays.toString(list));
}
3.4 适用场景
- 平均时间复杂度为:O(n2) ; 空间复杂度为:O(1) ;稳定排序。
- 对于数据量少的情况,常常把插入排序作为快速排序的 补充。
- 如果数据序列基本有序或者数据量小,使用插入排序会非常高效。
4. 希尔排序
4.1 基本原理
- 对于直接插入排序而言,当数基本有序或者数据量少的时候,能够达到很快的时间复杂度,但是如果是随机无序,那么就会非常慢。
- 希尔排序就是改进直接插入排序的功能:首先改变增量的大小,是的对数据少的数进行直接插入排序,随着增量的变小,数据量虽然越来越多,但是数据越来越有序,能够完美达到使德直接插入排序好的功能。
4.2 过程步骤
直接插入排序可看做默认增量变量为x=1,对x个数组,使用直接插入方法进行排序。
希尔排序初始增量变量为k,对k个数组使用直接插入方法进行排序,k的值不断减小,直到为一。
4.3 代码实现
public class 希尔排序 {
public static void main(String[] args) {
//直接插入
int[] list=new int[]{3,5,6,1,2,4,8,9,7};
method3(list);
}
//希尔排序基于方法三:
public static void method3(int[] list){
int x;
for(x=list.length/2;x>=1; x=x/2) {
//循环一:需要插入元素的位置遍历
for (int i = x; i < list.length; i++) { //需要插入的数
int temp = list[i]; //需要插入的数
int j = i - x;//指向插入前面的一个元素
//循环二的作用:从后向前比较,如果大于插入值,就先后移动
for (; j >= 0 && list[j] > temp; j=j-x) {
list[j + x] = list[j];
}
list[j + x] = temp;
}
}
System.out.println("list="+ Arrays.toString(list));
}
/**
* 核心功能:希尔排序,在插入排序上面的改进
*/
public class demo4 {
public static void main(String[] args) {
int[] data={7,6,5,4,3,2,1};
//设置步数:插入排序即为1
for(int i=data.length/2;i>=1;i=i/2){
//这两层循环和插入排序一模一样
for(int j=i;j<data.length;j+=i){
for(int z=j;z>=i;z-=i){
if(data[z]<data[z-i]){
int temp=data[z];
data[z]=data[z-i];
data[z-i]=temp;
}
}
}
System.out.println("i="+i+" "+Arrays.toString(data));
}
System.out.println(Arrays.toString(data));
}
}
4.4 适用场景
- 平均时间复杂度为:O(n1.5-2) ; 空间复杂度为:O(1) ;不稳定排序。
- 希尔排序是**第一个突破O(n2)**的排序算法,是简单插入排序的改良版本
- 希尔排序的时间复杂度和增量的取值有关,时间复杂度最好可看做:O(n1.5).
- 常见的增量序列:[1,2,4,8,10] ; {1, 3, …, 2k-1}; (1, 5, 19, 41, 109,…),4i - 3*2i + 1
5. 堆排序
5.0 概念介绍
堆结构: 具有以下性质的完全二叉树
- 大顶堆:每个节点都大于或者等于它的左右孩子节点
- 小顶堆:每个节点都小于或者等于它的左右孩子节点
完全二叉树的优秀性质:
4. 完全二叉树有n个节点,那么就有n/2个非叶子节点
5. 第i个节点的双亲节点为/2
6. 第i个节点的连个孩子节点为:2i+1和2i+2(如果存在的话)
5.1 基本原理
- 通过构建堆,依次取出堆顶元素,然后重新构造,再取出堆顶元素,得到有序序列
5.2 过程步骤
- 将待排序的序列构造为一个大顶堆,序列的最大值为堆顶的根节点,将它已到系列末尾,
- 然后将剩下的n-1个序列重新构造为一个堆,得到n个元素的次大值,
- 反复进行,最终得到有序序列。
代码实际步骤
- 首先得到非叶子节点n/2,然后遍历,依次调整位置。(初始值进行堆调整,得到一个大顶堆)
- 需要循环n-1次,依次把大顶堆的最大值交换到序列末尾。
- 调整函数的书写:首先判断根节点是否是最大(与孩子节点相比),不是最大就要调换顺序,然后进行递归。
5.3 代码实现
public class 堆排序 {
public static void main(String[] args) {
//堆排序
int[] list = new int[]{3, 5, 6, 1, 2, 4, 8, 9, 7};
Heap_Sort(list);
System.out.println("list结尾="+ Arrays.toString(list));
}
//堆排序主方法:
public static void Heap_Sort(int[] list){
//1)对初始数据进行堆调整,获得大顶堆
System.out.println("list初始="+ Arrays.toString(list));
for(int i=list.length/2;i>=1;i--){
System.out.println("i-1="+(i-1));
Heap_Adjust(list,i-1,list.length); //i-1为下标值
}
System.out.println("list初始="+ Arrays.toString(list));
//2)取出大顶堆的堆顶,然后再进行堆调整(这个过程需要重复n-1次,进行微调)
for(int j=list.length;j>1;j--){
swop(list,0,j-1); //首位置与末位置进行交换
Heap_Adjust(list,0,j-1); //堆进行调整,需要调整的堆大小要减一
}
}
//堆的子方法:堆调整
public static void Heap_Adjust(int[] list,int head,int length){
//1)左节点和右节点位置
int left=2*head+1;
int right=2*head+2;
int maxIndex=head; //假设原来的位置最大
//2)如果左孩子和有孩子存在,比较值,替换
if(left<length && list[left]>list[maxIndex]) maxIndex=left;
if(right<length && list[right]>list[maxIndex]) maxIndex=right;
//3)如果最大值位置变更了,需要交换,并递归操作下去
if(maxIndex!=head){
//交换
swop(list,head,maxIndex);
System.out.println("list交换时="+ Arrays.toString(list));
//递归
Heap_Adjust(list,maxIndex,length);
}
}
//堆的子方法:交换元素(list[i]与list[i]
public static void swop(int[] list, int i,int j){
int temp=list[i];
list[i]=list[j];
list[j]= temp;
}
}
5.4 适用场景
- 平均时间复杂度为:O(nlogn) ; 空间复杂度为:O(1) ;不稳定排序。
- 尤其是在解决诸如**“前n大的数”一类问题时**,几乎是首选算法。
6. 归并排序
6.1 基本原理
![](https://i-blog.csdnimg.cn/blog_migrate/86c8db7f28ea90ee332630509934b946.png)
- 采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列
6.2 过程步骤
- 申请一个和序列相等的空间,用于存放临时数据
- 把序列进行对半平分,得到有序的左半边和有序的右半边,之后再进行合并(有合并方法)。
- 对步骤二中的左半边和右半边进行递归操作,直到处于有序状态。
- 算法核心:处理两个序列的合并部分,需要使用两个指针进行操作。
6.3 代码实现
package paixu;
import java.util.Arrays;
public class 归并排序 {
public static void main(String[] args) {
//归并排序
int[] list = new int[]{3, 5, 6, 1, 2, 4, 8, 9, 7};
mergeSort(list);
System.out.println("list="+ Arrays.toString(list));
}
//1)主函数:顺便开辟新的空间
public static void mergeSort(int[] arr){
int[] temp =new int[arr.length];
internalMergeSort(arr, temp, 0, arr.length-1);
}
//2)序列进行划分
private static void internalMergeSort(int[] arr, int[] temp, int left, int right){
//当left==right的时,已经不需要再划分了
if (left<right){
int middle = (left+right)/2;
internalMergeSort(arr, temp, left, middle); //左子数组
internalMergeSort(arr, temp, middle+1, right); //右子数组
mergeSortedArray(arr, temp, left, middle, right); //合并两个子数组
}
}
// 3)合并两个有序子序列
private static void mergeSortedArray(int arr[], int temp[], int left, int middle, int right){
int i=left;
int j=middle+1;
int k=0;
while (i<=middle && j<=right){
temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
}
while (i <=middle){
temp[k++] = arr[i++];
}
while ( j<=right){
temp[k++] = arr[j++];
}
//把数据复制回原数组(嵌套已经排好序的部分)
for (i=0; i<k; ++i){
arr[left+i] = temp[i];
}
}
}
6.4 适用场景
- 平均时间复杂度为:O(nlogn) ; 空间复杂度为:O(n) ;稳定排序。
6.5 代码优化
7. 快速排序
7.1 基本原理
- 通过一趟排序,将待排记录分割为独立的两个部分,其中一部分关键字均比另一部分关键字小,然后通过递归,再分别对这两个部分进行分割排序,最终达到整个序列有序目的。
7.2 过程步骤
- 先从序列中选出一个数作为基准
- 两个指针指向序列的头和尾,如果左边指针比基准值大,于此同时右边指针比基准值小,则交换两个数据,直到两个指针走到一起(这个时候指针的位置是基准应该在的位置)
- 基准本应该在的位置的值和基准实际在的位置的值进行交换。(这时基准位置的左边都是比基准小的数,基准右边都是比基准大的数)
- 以基准为分割线,分为两个区,对每个区重复2,3操作。
简便步骤
- 先从序列中选出一个数作为基准值;
- 将比基准值小的数都放到它的左边,比基准值大的数都放到它的右边。
- 以基准值分为两个区,再分别对这两个区重复操作1和2,直到各区间只有一个数。
7.3 代码实现
public class 快速排序 {
public static void main(String[] args) {
//直接插入
int[] list = new int[]{3, 5, 6, 1, 2, 4, 8, 9, 7};
quick_sort(list, 0, list.length - 1);
System.out.println("list="+ Arrays.toString(list));
}
//方法:快速排序(使用方法递归)
public static void quick_sort(int[] list, int begin, int end) {
//1)截止条件
if (begin > end) return;
//2)递归中心
int left = begin;
int right = end;
int temp = list[left]; //随机挑选一名幸运观众
//(0)交换顺序,使得小于基准的都在左边,大于基准的都在右边,直到找到基准适合位置
while (left < right) {
//(1)右边要小于,左边要大于
while(list[right] > temp && right>left) right--;
while(list[left] <= temp && right>left) left++;
//(2)交换
if(right>left){
int tem = list[left];
list[left] = list[right];
list[right] = tem;
}
}
//(3)把找到的幸运观众也放到合适的位置上
list[begin]=list[left];
list[left]=temp;
quick_sort(list, begin, left-1); //右边要减一
quick_sort(list, left+1, end); //左边要加一
}
}
7.4 适用场景
- 平均时间复杂度为:O(nlogn) ; 空间复杂度为:O(logn) ;不稳定排序。
- 快速排序其实能够算作是冒泡排序的改进。
- 第一个实现O(nlogn) 时间复杂度
7.5 代码优化
- 优化选取枢轴:前面算法默认选取第一个数,这里可以采用方法:随机选取,三数取中,九数取中
- 优化小数组时的排序方案:当数比较少的时候,没必要用快排,可以使用直接插入排序,通过一个判断长度判断实现。
- 优化递归操作:减少不必要的递归。
8. 基于快速排序的TopK
- 要点:主要是在递归的时候,left+1的值与K进行比较
1)当left+1==K,即找到了K个,直接返回。
2)当left+1>K,即多了,则要再细分,尾巴变为left-1
3)当left+1<K,即少了,则要扩大范围,头部变为left+1 - 思路
1)特殊情况
2)一般情况(交换)
3)把要比较的那个值也放到对应位置
4)递归操作(TopK还是全排列,区别就在这里) - 代码
public static void method(int[] data,int first,int end,int K){
//1)特殊情况
if(first>end) return;
int left=first;
int right=end;
int comindex=first;
//2)一般情况(先右边,后左边,左边需要去等号)
while(left<right){
while(data[right]>data[first] && left<right) right--;
while(data[left]<=data[first] && left<right) left++;
//交换
int temp=data[left];
data[left]=data[right];
data[right]=temp;
}
//3)与比较值交换first,left.
int temp1=data[first];
data[first]=data[left];
data[left]=temp1;
//4)递归(全排序)
// method(data,first,left-1,K);
// method(data,left+1,data.length-1,K);
//4)递归规则的改变(TopK排序)(K不用边,因为序标是不变的)
//注释:这里递归不能使用原来的left,不然相同时会陷入死循环
if(left+1==K){
return;
}else if(left+1>K){
method(data,first,left-1,K);
}else{
method(data,left+1,data.length-1,K);
}
}
9. 总结
9.1 算法的时间空间复杂度
综合:各大排序算法的时间复杂度和空间复杂度
不稳定的排序:快选希堆(快排,选择排序,希尔排序和堆排序)
最好用的排序(快速排序)
9.2 各类排序算法的使用场景
????
主要参考文章(如有侵权,联系立删):
https://zhuanlan.zhihu.com/p/42586566
https://www.runoob.com/w3cnote/sort-algorithm-summary.html
https://blog.csdn.net/weixin_43824059/article/details/88238177
快速排序:讲解详细
堆和堆排序:讲解详细
https://blog.csdn.net/zxzxzx0119/article/details/79826380
希尔排序