目录
1.排序是什么?
2.排序的稳定性
3.七大排序的时间复杂度与空间复杂度
4.选择排序
5.插入排序(O(n^2)) 类似于打牌(稳定)
6.希尔排序:缩小增量的排序(不稳定)
7.归并排序(和希尔排序很像)
8.快速排序(基于算法导论中的分区思想)重要
8.1随机化快排的问题:
8.2快排的非递归实现
8.3二路快排
8.4挖坑法分区方法代码(直到在纸上如何画图即可)
8.5三路快排 (了解)
9.堆排序(非常稳定)
10.冒泡排序(时间复杂度o(n^2))
11.面试题:海量数据的排序处理(常考)
题外话:设置注释
七大排序代码:
排序就是一串记录,按照其中的某个或者某些关键字的大小,递增或递减的排列起来的操作。
两个相等的数据,如果经过排序,排序算法能保证其相对位置不发生变化,则我们称该算法是具备稳定性的排序算法。
为什么稳定性这么重要?
这里都是内部排序(一次性将所有待排序的数据放入内存中进行排序,基于元素之间的比较的排序)
而外部排序 :依赖硬盘(外部存储器)进行的排序算法,桶排序、基数排序、基数排序(对于数据集合的要求非常高,只能在特定的场合下使用)。时间复杂度都是O(n),应用场景非常局限,只能在特定的场景下去应用。内存放不下,需要外部存储。
器,称为海量排序算法。
每次从无序区间中选择一个最大或最小值,存放在无需区间的最前或者最后的位置(此位置的元素已经有序) ,直到所有的数据都排序结束。
为什么选择排序不稳定?
选择排序的进阶版本
双向选择排序,一次排序过程中,同时选出最大值和最小值,放在无需区间的最后和最前。
思路:将集合分为俩个区间,i是当前已经便利的元素,一个已经排序的区间[0...i),一个待排序的区间(i...n]。
每次从待排序区间找到一个元素,放入已排序的区间的合适位置,直到整个数组有序。
在近乎有序的数组下,插入排序的数组性能非常好,甚至优于nlog(n)的排序。
插入排序和选择排序最大的不同在于:当插入排序当前遍历的元素>前驱元素时,此时可以提前结束内层循环。
极端场景下,当集合是一个完全有序的集合,插入排序内层循环一次都不走,插入排序时间复杂度变为O(n)。
插入排序经常用作高阶排序算法的优化手段之一
了解:插入排序优化的手段:(用二分查找法)
折半插入排序:
首先:二分查找的前提是该集合必须是一个有序的集合。
而在插入排序中,我们每次都是在有序区间中找一个合适的插入位置,所以可以使用二分查找法来定位元素的插入位置。
这里注意这个公式为什么要这么写:(是为了防止溢出)
先选定一个整数(gap,gap一般都选数组长度的一半或者1/3)
将待排序的数组先按照gap分组,不同组之间使用插入排序,排序之后,再将gap/==2或者gap/=3,重复上述的流程,直到gap=1
当gap=1时,整个数组已经被调整的近乎有序,此时就是插入排序最好的场景,最后再在整个数据上进行一次插入排序。
插入排序和希尔排序,当gap=1时,公式完全一样。
希尔排序为什么要不断地向前比较,能否从零开始向后看gap步?
例:3-----2-----1
不断向后比较
2-----3-----1
2-----1-----3
这样的话,2就没办法换到合适的位置了
思想:将原数组不断拆分,一直拆到每个子数组只有一个元素,第一个阶段结束,归而为1这个阶段结束。
并:将相邻的两个数组合并为一个有序的数组,直到整个数据有序。
分:用的是递归
并:第一步:先创建一个大小为合并之后数组大小的临时数组aux,将数组的值拷贝过去。
第二步:如下
为何合并过程需要创建一个新的aux数组?
防止在合并过程中,因为小元素要覆盖大的元素,丢失某些元素
归并排序是一个稳定的nlogN排序算法,此处的稳定指的是时间复杂度稳定(无论集合中的元素如何变化,归并排序的时间复杂度一直都是nlogN,不会退化为O(N^2))且归并排序也是一个稳定性的排序算法(相等元素的先后顺序并不会发生改变)。
(递归类似于树结构,n不断的2/2/2/2,直到只剩下一个元素,这就是logN(树的高度)级别的样子)
递归的深度就是我们拆分数组所用的时间,就是树的高度,logN。
public static void mergeSort(int []arr){ mergeSortInternal(arr,0,arr.length-1); } //在arr[l..r]进行归并排序 private static void mergeSortInternal(int[] arr, int l, int r) { if(l>=r){ //当前数组只剩下一个元素,归过程就结束了 return; } int mid=l+((r-1)>>1); //将原数组拆分为左右俩个小区间,分别递归进行归并排序 mergeSortInternal(arr,l,mid); mergeSortInternal(arr,mid+1,r); //merge操作 merge(arr,l,mid,r); }
而o(n)来自于合并两个子数组的过程merge,就是一个数组的遍历
综上为非常稳定的nlogN。
归并排序的两点优化:
1.当左右两个子区间走完子函数后,左右两个区间已经有序了
如果此时arr[mid]
arr[mid]已经是左区间的最大值
arr[mid+1]已经是右区间的最小值
=>整个区间已经有序了,每必要在执行merge过程,只有左右俩个区间还有先后顺序不同时,才merge。
2.在小区间上,我们可以直接使用插入排序来优化,没必要一直拆分到1位置
r-1<=15,使用插入排序性能很好的。
可以减少归并的递归次数 。
改进:
随机选择的代码:
8.1随机化快排的问题:
当待排序的集合中出现大量重复问题时,就会导致某个分区的元素过多(相等的元素全在一个区间中),造成递归过程中,递归树严重不平衡,快排倒退为O(n^2)。
解决办法,把相同的元素均衡在俩个区间。(二路快排)
什么是栈溢出?
几数取中,一般都是三数取中,在arr[left],arr[mid],arr[right]中选择一个中间值作为基准
2,3都能避免递归过程中在接近有序的数组上,快速排序分区严重不平衡的问题
8.2有大量重复数据的数组上,快排仍然会退化。
原因:极端情况,当100w个元素全都是一个值的时候,分区之后,
=v的集合中,二叉树又退化成了单支树。
代码如下:
public static void quickSortNonRecursion(int[] arr){ Deque
stack=new ArrayDeque<>(); //栈中保存当前集合的开始位置和中止位置 int l=0; int r=arr.length-1; stack.push(r); stack.push(l); while(!stack.isEmpty()){ //栈不为空,说明子区间还没有处理完毕 int left=stack.pop(); int right=stack.pop(); if(left>=right){ //区间只有一个元素 continue; } int p=partition(arr,left,right); //依次将右区间的开始和结束位置入栈 stack.push(right); stack.push(p+1); //再将左侧区间的开始和结束位置入栈 stack.push(p-1); stack.push(left); } }
思路:将相等的元素均分到两个子区间
private static int partition2(int[] arr, int l, int r) { int randomIndex=random.nextInt(l,r); swap(arr,l,randomIndex); int v=arr[l]; //arr[l+1..i]<=v //[l+1..l+1)=0 int i=l+1; //arr(j..r]>=v //(r..r]=0; int j=r; while(true){ //i从前向后扫描,碰到第一个>=v的元素停止 while(i<=j&&arr[i]
v) { j--; } if(i>=j){ break; } swap(arr,i,j); i++; j--; } //j落在最后一个<=v的元素身上 swap(arr,l,j); return j; }
这种方法没有元素交换,只有赋值,理论上会快一点。
这个思路主要用在笔试选择题。
注意:挖坑法必须要先从后向前扫描,再从前向后扫描。
如果是从前向后扫描,就会有问题,会把大的值换到最前面去
结果:
在一次分区函数的操作中,把所有相等的元素都放在最终位置,只需要在大于v和小于v的子区间上进行快排,所有相等的元素就不再处理了。
思想:以最大堆为例,依次取出最大值,可得到降序数组,无法在原数组上进行排序,还得创建一个和当前数组相同的堆,堆的时间复杂度为o(n)。
冒泡排序代码:
假设现在待排序的数据有100G,但是内存只有1GB,如何排序这100GB的数据呢?
12 几种快排总结
快速排序是不稳定的。
nlogn,n是插入排序的insertionSort()方法,logn是递归函数的调用次数(树的高度)。
package sort; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Deque; import java.util.concurrent.ThreadLocalRandom; public class SevenSort { public static final ThreadLocalRandom random=ThreadLocalRandom.current(); /* * 选择排序,时间复杂度o(n) * */ public static void selectionSort(int [] arr){ //最开始无序区[0...n],有序区间为[] //当无序区间只剩下一个元素时,整个集合已经有序。所以length-1 for(int i=0;i
arr[max]) { max = i; } } //min索引现在一定是当前区间的最小值索引,与low交换位置 swap(arr, low, min); if (max == low) { //最大值已经被换到min的位置 max = min; } //最大值落在了high的位置 swap(arr, max, high); low += 1; high -= 1; } } /* * 直接插入排序 * 每次从无序区间中拿第一个值插入到已经排序区间的合适位置,直到整个数组有序。 *已排序区间[0..i) * 待排序区间[i...n] * */ public static void insertionSort(int [] arr) { for (int i = 1; i < arr.length; i++) { //待排序区间的第一个元素arr[i] //从待排序区间的第一个元素向前看,找到合适的插入位置 // for (int j = i; j >0; j--) { // //arr[j-1]已排序区间的最后一个元素 // if(arr[j]>=arr[j-1]){ // //相等也不交换,保证稳定性 // //此时说明arr[j]>已排序区间的最大值,说明arr[j]已经有序了 // //直接下次循环 // break; // }else{ // swap(arr,j,j-1); // } // } //更简单的写法,和上面一样 for (int j = i; j >= 1 && arr[j] < arr[j - 1]; j--) { swap(arr, j, j - 1); } } } /* * 折半查找法 * */ public static void insertionSortBS(int [] arr){ //有序区间[0..i) //无序区间[i...n] for (int i = 1; i < arr.length ; i++) { //val当前无序区间中的一个值 int val=arr[i]; int left=0; int right=i; while(left >1); if(val =arr[mid] left=mid+1; } } //搬移left...i的元素 for (int j=i;j>left;j--){ arr[j]=arr[j-1]; } //left就是val插入的位置 arr[left]=val; } } /* * 希尔排序 * */ public static void shellSort(int [] arr){ int gap= arr.length >>1; while(gap>1){ //预处理阶段 insertionSortByGap(arr,gap); gap=gap>>1; } //此时gap等于1,整个集合已经接近有序,只需要全集合来一次插入排序即可 insertionSort(arr); } /* *按gap分组进行插入排序 * */ private static void insertionSortByGap(int[] arr, int gap) { for (int i = gap; i < arr.length ; i++) { //不断向前扫描相同的gap的元素 //j-gap从j位置开始向前还有相同步数的元素 for (int j = i; j-gap>=0&&arr[j] =r){ //当前数组只剩下一个元素,归过程就结束了 return; } int mid=l+((r-1)>>1); //将原数组拆分为左右俩个小区间,分别递归进行归并排序 //走完这个函数之后arr[l..mid]已经有序 mergeSortInternal(arr,l,mid); mergeSortInternal(arr,mid+1,r); //只有左右俩个区间还有先后顺序不同时,才merge。 //merge操作 if(arr[mid]>arr[mid]+1) { merge(arr, l, mid, r); } } /* * 合并俩个子数组arr[l..mid]和arr[mid+l...r] * 为一个大的有序数组 * */ public static void merge(int[] arr,int l,int mid,int r){ if(r-l<=15){ //小区间直接插入排序 insertionSort(arr,l,r); return; } //先创建一个临时数组 int[] aux =new int[r-l+1]; //将arr元素拷贝到aux上 for (int i = 0; i < aux.length; i++) { //新数组与原数组差了l个偏移量 arr[i] = arr[i + l]; } //i 表示左侧小数组的开始索引 //j 表示右侧小数组的开始索引 int i=1; int j=mid+1; //原地进行arr数组的合并,k表示当前合并原数组的索引下标 for (int k = l; k <=r ; k++) { if(i>mid){ //左侧期间已经处理完毕,只需要将右侧区间的值拷贝到原数组即可 arr[k]=aux[j-1]; j++; }else if(j>r){ //右侧区间已经处理完毕,只需要将左侧区间的值拷贝到原数组即可 arr[k]=aux[i-1]; }else if(aux[i-1]<=aux[j-1]){ //此时左侧区间的元素值较小,相等元素放在左区间保持稳定性 arr[k]=aux[i-1]; i++; }else{ //右侧区间的元素较小 arr[k]=aux[j-1]; j++; } } } /* * 在arr[l..r]使用插入排序 * */ private static void insertionSort(int[] arr, int l, int r) { for (int i = l+1; i <=r; i++) { for (int j=i;j =r){ // return; // } //先获取分区点 //所谓分区点就是经过分区函数后,某个元素落在了最终位置 //分区点左侧全都是小于该元素的区间,分区点右侧全都是>=该元素的区间 int p=partition(arr,l,r); //递归重复在左区间和右区间重复上述流程 quickSortInternal(arr, l, p-1); quickSortInternal(arr, p+1, r); } //在arr[l..r]上的分区函数,返回分区点的索引 private static int partition(int[] arr, int l, int r) { //随机在当前数组中选一个数 int randomIndex=random.nextInt(l,r); swap(arr,l,randomIndex); int v=arr[l]; //arr[l+1,j] =v //i表示当前正在扫描的元素 int j=l; for (int i = l+1; i <=r ; i++) { if(arr[i] stack=new ArrayDeque<>(); //栈中保存当前集合的开始位置和中止位置 int l=0; int r=arr.length-1; //先入右后入左,所以先出左后出右 stack.push(r); stack.push(l); while(!stack.isEmpty()){ //栈不为空,说明子区间还没有处理完毕 int left=stack.pop(); int right=stack.pop(); if(left>=right){ //区间只有一个元素 continue; } int p=partition(arr,left,right); //依次将右区间的开始和结束位置入栈 stack.push(right); stack.push(p+1); //再将左侧区间的开始和结束位置入栈 stack.push(p-1); stack.push(left); } } /* *双路快排 * */ public static void quickSort2(int []arr){ quickSortInternal2(arr,0,arr.length-1); } private static void quickSortInternal2(int[] arr, int l, int r) { if(r-l<=16){ insertionSort(arr,l,r); return; } int p=partition2(arr,l,r); quickSortInternal2(arr,l,p-1); quickSortInternal(arr,p+1,r); } /* * 二路开排的分区 * */ private static int partition2(int[] arr, int l, int r) { int randomIndex=random.nextInt(l,r); swap(arr,l,randomIndex); int v=arr[l]; //arr[l+1..i]<=v //[l+1..l+1)=0 int i=l+1; //arr(j..r]>=v //(r..r]=0; int j=r; while(true){ //i从前向后扫描,碰到第一个>=v的元素停止 while(i<=j&&arr[i] v) { j--; } if(i>=j){ break; } swap(arr,i,j); i++; j--; } //j落在最后一个<=v的元素身上 swap(arr,l,j); return j; } /* * 三路快排 * */ public static void quickSort3(int[]arr){ quickSortInternal3(arr,0,arr.length-1); } public static void quickSortInternal3(int[] arr,int l,int r){ if(r-l<=15){ insertionSort(arr,l,r); return; } int randomIndex =random.nextInt(l,r); swap(arr,l,randomIndex); int v=arr[l]; //这些变量的取值一定满足区间定义 //arr[l+1..lt] v //gt是第一个>v的元素 int gt=r+1; //i从前向后扫描和gt重合时,所有元素就处理完毕 while(i v){ //交换到gt-1 swap(arr,i,gt-1); gt--; //此处i不++,交换来的gt-1还没有处理 }else{ //此时arr[i]=v i++; } } //lt落在最后一个 v quickSortInternal3(arr,gt,r); } /* * 堆排序 * */ public static void heapSort(int []arr){ for (int i =(arr.length-1-1)/2; i >=0 ; i--) { siftDown(arr,i,arr.length); } //此时数组已经被调整为最大堆 for(int i=arr.length-1;i>0;i--){ //arr[0]栈顶元素,为当前堆的最大值 swap(arr,0,i); siftDown(arr,0,i); } } /* * 冒泡排序 * */ public static void bubbleSort(int []arr){ for (int i = 0; i < arr.length; i++) { boolean isSwaped =false; for(int j=0;j< arr.length-1-i;j++){ if(arr[j]>arr[j+1]){ swap(arr,j,i+1); isSwaped=true; } } } } /*元素下沉操作 * */ //length数组长度 public static void siftDown(int[] arr,int i,int length){ while(2*i+1 arr[j]) { j=j+1; } //j是左右子树的最大值 if(arr[i]>arr[j]){ //下沉结束 break; }else{ swap(arr,i,j); } } } private static void swap(int []arr,int i,int j){ int temp =arr[i]; arr[i]=arr[j]; arr[j]=temp; } //普通测试 public static void main(String[] args){ int []arr={9,2,5,7,5,4,3,6}; selectionSort(arr); System.out.println(Arrays.toString(arr)); } /* //运行测试代码 public static void main(String [] args ) { int n = 10000; int[] arr = new int[n]; ThreadLocalRandom random = ThreadLocalRandom.current(); for (int i = 0; i < n; i++) { arr[i] = random.nextInt(0, Integer.MAX_VALUE); } int [] arr1 =Arrays.copyOf(arr,n); //当前的系统时间 long start =System.nanoTime(); heapSort(arr1); long end =System.nanoTime(); if(isSorted(arr1)){ System.out.println("冒泡排序共耗时:"+(end-start)/1000000+"ms"); } } *//* int[] arr ={7,4,5,3,2,1,9,10,8,6}; heapSort(arr); System.out.println(Arrays.toString(arr));*//* //性能测试代码 public static boolean isSorted(int []arr){ for(int i=0;i arr[i+1]){ System.out.println("sort error"); return false; } } return true; }*/ }