数据结构——排序算法全解析(入门到精通)
一:排序的概念及引⽤
1.1 排序的概念
排序:所谓排序,就是使⼀串记录,按照其中的某个或某些关键字的⼤⼩,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,⽽在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的
说简单一点:如果序列中存在两个值相同的元素,排序前和排序后,这两个元素的相对顺序不变,就认为是稳定顺序,否则就是不稳定
举个例子:9 5 2a 8 2b 3
排序之前,2a在2b前面,排序后也能保证2a在2b前面,就认为稳定。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断的在内外存之间移动数据的排序。(要排序的元素太多了,内存存不下,借助硬盘。)
内存和硬盘的区别
首先他们都能存储数据
区别:
1内存访问速度快、硬盘访问速度满
2内存存储空间比硬盘小
3内存比硬盘贵
4内存上的数据断电后消失,硬盘的数据断点后还在(持久化存储)
1.2 排序运⽤
购物中常见的手机价格排序
1.3 常⻅的排序算法
二.:常⻅排序算法的实现
2.1 插⼊排序
2.1.1 基本思想:
直接插⼊排序是⼀种简单的插⼊排序法,其基本思想是:
把待排序的记录按其关键码值的⼤⼩逐个插⼊到⼀个已经排好序的有序序列中,直到所有的记录插⼊完为⽌,得到⼀个新的有序序列 。实际中我们玩扑克牌时,就⽤了插⼊排序的思想。
2.1.2 直接插⼊排序
当插⼊第i(i>=1)个元素时,前⾯的array[0],array[1],R,array[i-1]已经排好序,此时⽤array[i]的排序码与array[i-1],array[i-2],R的排序码顺序进⾏⽐较,找到插⼊位置即将array[i]插⼊,原来位置上的元素顺序后移
往顺序表中插入,就会触发搬运,元素一个一个向后移。
给定一个数组,把数组分为两个部分
1:有序区间(已排序区间)
2:无序区间(待排序区间)
初始情况下,这个数组是未经排序的,此时认为有序区间为空,无序区间就是整个数组。
每次选择无序区间的一个元素,把这个元素往前插入到有序区间的合适位置,然后有序区间变多
无序区间元素变少,最后慢慢变为有序
上代码: public static void insertSort(int[] arr){ //bound 变量表示边界 //【0,bound)是有序区间 //【bound,arr.length)是无序区间 //此处是无序区间 for(int bound=1;bound<arr.length;bound++){ //取无序区间第一个元素 int value= arr[bound]; //从有序区间的最后一个元素开始比较 int cur=bound-1;//取有序区间最后一个元素的下标 //从右边的第一个元素开始比较,直到找到合适的位置 for(;cur>=0;cur--){ //如果当前元素大于要插入的元素,则将当前元素向后移动一位 if(arr[cur]>value){ arr[cur+1]=arr[cur]; }else{ break;} } arr[cur+1]=value; } } public static void main(String[] args) { //验证插入排序 int[] arr={9,5,2,7,3,6,8}; insertSort(arr); System.out.println(Arrays.toString(arr));//[2, 3, 5, 6, 7, 8, 9]
直接插⼊排序的特性总结:
1. 元素集合越接近有序,直接插⼊排序算法的时间效率越⾼
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1),它是⼀种稳定的排序算法
4. 稳定性:稳定
2.1.3 希尔排序( 缩⼩增量排序)
希尔排序法⼜称缩⼩增量法。希尔排序法的基本思想是:先选定⼀个整数,把待排序⽂件中所有记录分成多个组,所有距离为的记录分在同⼀组内,并对每⼀组内的记录进⾏排序。然后,取,重复上述分组和排序的⼯作。当到达=1时,所有记录在统⼀组内排好序。
单一点就是 分组进行插入排序
先把整个数组,分成若干组,针对每一组进行插入排序~~
引入gap(间隙)这样的概念
比如设gap为3
9 5 2 7 3 6 8 10 20 9 7 10 为一组 5 3 10为一组 2 6 20 为一组
如果gap为1,就表示所有元素都在同一个数组
gap既表示同组元素之间的间隔,也表示分出来的组数怎么做呢?
希尔排序,分组插入排序,不是一次而是若干次
比如指定gap为3 2 1 这样的序列
先按照gap为3进行分组插排
然后gap为2
最后gap为1,肯定保证有序
gap有大佬规定的希尔序列,size/2、size/4、size/8、size/16
代码实现: //希尔排序 //分组,对每组进行插入排序 //分组的大小为gap,gap为数组长度的一半 //此处使用希尔序列 gap=size/2,size/4,size/8 public static void shellSort(int[] arr){ int gap=arr.length/2; while(gap>=1){ insertSortgap(arr,gap); gap/=2; } } public static void insertSortgap(int[] arr,int gap){ for(int bound=gap;bound<arr.length;bound++){ //value表示gap表示的第一个元素 int value=arr[bound]; //cue取出value所在组的前一个元素的下标 int cur=bound-gap; for(;cur>=0;cur-=gap){ if(arr[cur]>value){ //将当前元素向后移动gap个位置 arr[cur+gap]=arr[cur]; }else{ break; } } arr[cur+gap]=value; } }
总结一下 也就是插入排序,我们找到gap位置的元素,然后再找到和gap一个组的元素他们间隔gap,然后减gap就得到了,然后判断两个值的大小,如果大于就交换,最后再更新一下。
希尔排序的特性总结:
1. 希尔排序是对直接插⼊排序的优化。
2. 当gap > 1时都是预排序,⽬的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体⽽⾔,可以达到优化的效果。我们实现后可以进⾏性能测试的对⽐。
3. 希尔排序的复杂度最坏是O(N方)
2.2 选择排序
2.2.1 基本思想:
每⼀次从待排序的数据元素中选出最⼩(或最⼤)的⼀个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
2.2.2 直接选择排序
在元素集合array[i]--array[n-1]中选择关键码最⼤(⼩)的数据元素
若它不是这组元素中的最后⼀个(第⼀个)元素,则将它与这组元素中的最后⼀个(第⼀个)元素交换
在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
把整个数组划分两个区间,前半部分已排序区间(有序区间),后半部分是待排区间(无序区间)
初始情况下,有序区间是空区间
从无序区间中,找到整个无序区间里的最小值,把这个最小值,和无序区间的第一个元素交换
同时把这个无序区间的第一个元素,划分到有序区间中。
每进行一趟,有序区间都会变大
代码实现: //选择排序核心思想 每次从待排序区间中找打最小值,放到已排序区间的末尾 //这个找最小值的过程称为打擂台,以待排序区间的第一个元素为起点,然后与其他元素挨个比较,取最小值 private static void selectSort(int[] arr){ //[0,bound)是已排序区间 //[bound,arr.length)是待排序区间 //此处是待排序区间 //当bound=arr.length-1时,表示已排序区间的右边界 for(int bound=0;bound<arr.length-1;bound++){ int min=bound; for(int i=bound+1;i<arr.length;i++){ if(arr[i]<arr[min]){ int temp=arr[i]; arr[i]=arr[min]; arr[min]=temp; } } System.out.println(bound+" "+Arrays.toString(arr)); } }
【直接选择排序的特性总结】
1. 直接选择排序思考⾮常好理解,但是效率不是很好。实际中很少使⽤
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
2.2.3 堆排序
堆排序(Heapsort)是指利⽤堆积树(堆)
代码实现: //堆排序 //建大堆(升序) //把堆顶元素和堆的最后一个元素交换位置,然后向下调整堆 public static void heapSort(int[] arr) { //1建堆 creatHeap(arr); //2依次把堆顶元素和待排序区间的最后一个元素交换 //此处我们设定【0 bound)为待排区间,也就是说堆顶元素为arr[0] int bound = arr.length - 1; for (int i = 0; i < arr.length; i++) { //交换堆顶元素和堆的最后一个元素 int temp = arr[0]; arr[0] = arr[bound]; arr[bound] = temp; //向下调整堆 shiftDown(arr, bound, 0); bound--; } } //建堆 public static void creatHeap(int[] arr) { //从最后一个非叶子节点 for(int i=(arr.length-1-1)/2;i>=0;i--){ shiftDown( arr,arr.length,i); } } //向下调整堆 //向下调整就是建大堆的过程 public static void shiftDown(int[] arr, int length, int index) { int parent = index; int child = 2 * parent + 1; while (child < length) { if (child + 1 < length && arr[child] < arr[child + 1]) { child++; } if (arr[parent] < arr[child]){ int temp = arr[parent]; arr[parent] = arr[child]; arr[child] = temp; }else{ break; } //更新 parent=child; child=parent*2+1; } }
这种数据结构所设计的⼀种排序算法,它是选择排序的⼀种。它是通过堆来进⾏选择数据。需要注意的是排升序要建⼤堆,排降序建⼩堆。
堆排序的基本思想:
1:针对整个数组,建立大堆(初始情况下,整个数组,都是待排序区间)
2:把堆顶元素和待排区间的最后一个元素交换,把最大元素放到已排序区间中
3:把堆顶元素进行向下调整,确保前面堆结构合法。重点介绍怎么创建堆,看我们需要什么,创建什么堆,然后排序
【堆排序的特性总结】1. 堆排序使⽤堆来选数,效率就⾼了很多。2. 时间复杂度:O(N*logN)3. 空间复杂度:O(1)4. 稳定性:不稳定
2.3 交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的⽐较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较⼤的记录向序列的尾部移动,键值较⼩的记录向序列的前部移动。
2.3.1 冒泡排序
比较交换相邻元素
这样一趟下来,就能把最大值或者最小值区分开来
上代码:冒泡之前就学习过还是很好理解的。
//冒泡排序 //比较两个相邻元素,如果前者大于后者,则交换位置 public static void bubbleSort(int[] arr) { for (int i = 0; i < arr.length - 1; i++) { for (int j = 0; j < arr.length - i - 1; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } }
【冒泡排序的特性总结】
1. 冒泡排序是⼀种⾮常容易理解的排序
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定
2.3.2 快速排序
快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序⽅法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两⼦序列,左⼦序列中所有元素均⼩于基准值,右⼦序列中所有元素均⼤于基准值,然后最左右⼦序列重复该过程,直到所有元素都排列在相应位置上为⽌。
总结一下:给定一个待排序的数组,从数组中选择一个基准值
拿着这个数组中的每个元素,都和基准值进行比较,把1整个数组,整理为三个部分
左侧:比基准值小
中间:基准值
右侧:比基准值大
然后把左侧的内容再进行递归,也分为三个部分,
右侧也是,也分为三个部分
然后继续继续递归,当递归到一定程度,足够小,有序性就可以保证了
// 假设按照升序对array数组中[left, right)区间中的元素进⾏排序void QuickSort(int[] array, int left, int right) {if(right - left <= 1)return;// 按照基准值对array数组的 [left, right)区间中的元素进⾏划分int div = partion(array, left, right);// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)// 递归排[left, div)QuickSort(array, left, div);// 递归排[div+1, right)QuickSort(array, div+1, right);}
上述为快速排序递归实现的主框架,发现与⼆叉树前序遍历规则⾮常像,同学们在写递归框架时可想想⼆叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进⾏划分的⽅式即可。
将区间按照基准值划分为左右两半部分的常⻅⽅式有:Hoare版、挖坑法、前后指针,我们主要讲解第一种。
//快速排序,辅助递归的方法 //此处约定[left,right]闭区间为处理待处理区间 private static void quickSort(int[] arr, int left, int right) { if(left>=right){ return ; } int index=partition(arr,left,right); //递归左版区间 quickSort(arr,left,index-1); //递归右版区间 quickSort(arr,index+1,right); } //这里需要注意选择基准数的位置,以及循环反向 //如果选取左侧的元素作为基准数,那么先从右往左找小于基准数的元素,再从左往右找大于基准数的元素 //如果选取右侧的元素作为基准数,那么先从左往右找大于基准数的元素,再从右往左找小于基准数的元素 private static int partition(int[] arr, int left, int right) { //选取最右侧的元素作为基准元素 int value=arr[right]; //设定两个下标,分别从左往右和从右往左 int i=left; int j=right; while(i<j){ //从左往右找大于基准元素的元素 while(i<j&&arr[i]<=value){ i++; } //从右往左找小于基准元素的元素 while(i<j&&arr[j]>=value){ j--; } //交换两个元素 if(i<j){ int temp=arr[i]; arr[i]=arr[j]; arr[j]=temp; } } //把重合的元素与基准值交换 //将基准元素放到中间 int temp=arr[i]; arr[i]=arr[right]; arr[right]=temp; return i; }
快速排序总结
1. 快速排序整体的综合性能和使⽤场景都是⽐较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定
2.4 归并排序
2.4.1 基本思想
归并排序(MERGE-SORT)是建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法(Divide and Conquer)的⼀个⾮常典型的应⽤。将已有序的⼦序列合并,得到完全有序的序列;即先使每个⼦序列有序,再使⼦序列段间有序。若将两个有序表合并成⼀个有序表,称为⼆路归并。 归并排序核⼼步骤:
随便给一个数组,首先把这个数组分成等长的两个部分
此时分出的两个部分,大概率不是有序的,此时不能直接合并
就需要对这两个数组AB再进一步拆分
比如把A拆成CD
B拆成EF
此时CDEF大概率也是无序的,也不能直接合并,再进一步拆分
当拆分到一定程度,比如拆分到两个数组长度为1,那就是有序的了,就可以合并了
代码实现://归并排序,辅助递归的方法//此处约定[left,right]闭区间为处理待处理区间private static void mergeSort(int[] arr, int left, int right) {//1:如果子区间中没有元素,或者只有一个元素,则不需要递归if(left>=right){return ;}//2:计算子区间的中间位置int mid=(left+right)/2;//3:递归左半边mergeSort(arr,left,mid);//4:递归右半边mergeSort(arr,mid+1,right);//5:合并两个有序子区间merge(arr,left,mid,right);}//合并两个有序子区间private static void merge(int[] arr, int left, int mid, int right) {//1:创建辅助数组int[] result = new int[right - left + 1];int resultSize=0;//2谁当两个下标指向每个区间的开通位置int cur = left;int cur2=mid+1;while(cur<=mid&&cur2<=right){if(arr[cur]<arr[cur2]){result[resultSize++]=arr[cur++];}else{result[resultSize++]=arr[cur2++];}}//3:将剩余元素添加到辅助数组while(cur<=mid){result[resultSize++]=arr[cur++];}while(cur2<=right){result[resultSize++]=arr[cur2++];}//4:将辅助数组中的元素复制回原数组for(int i=0;i<resultSize;i++) {arr[left + i] = result[i];}}
归并排序总结
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定
三 :排序算法复杂度及稳定性分析
也就是将几种排序分类,有快排,归并使用分治的思想,还有时间复杂度,空间复杂度,可以看出来,堆、快排、归并时间复杂度都是比较快的,
记住几个特例,会写代码就可以。