目录
排序的概念
概念
排序,就是使一串记录,按照其中的某个或者某些关键字,递增或者递减起来的操作。平时如果提到排序,通常指的是排升序(非降序),通常意义上的排序都是指原地排序,原地排序就是不开辟额外的数组空间进行的排序
- 严格升序:不包含重复元素,数字由小到大依次排序 i1<i2
- 严格降序:不包含重复元素,数字由大到小依次排序 i1>i2
- 非降序:前一个元素肯定<=后一个元素
- 非升序:前一个元素肯定>=后一个元素
稳定性
待排序的序列中,如果存在值相同的元素,经过排序后,值相等的元素的先后顺序并没有改变,那么这个排序算法是稳定的
排序的分类
内部排序:待排序的元素都存放在内存中,我们讲的七大排序都是内排序
外部排序:数据存储在硬盘中,每次排序都需要从硬盘中读取一部分内容到内存中,把这部分内容排序之后写回硬盘,有桶排序,基数排序,计数排序,这三个算法不具备普遍性,都需要特殊的场景才能使用
七大排序
- 现在主流排序算法都是在使用TimSort(插入排序+优化的归并排序)
算法复杂度
冒泡排序
原理:
默认我们排序的是升序,
- 我们将整个区间分为两个区间,一个为排序好的区间,默认是[ ],然后乱序的空间是整个数组[0,n-1],
- 每次我们通过从乱序区间的开头开始,两两元素比较,将较大的值往后面放,每遍历一次乱序的区间,就能将乱序区间的最大值放到乱序区间的最后面,然后乱序区间的长度减一,有序区间的长度加1,
- 直到乱序空间为空,那么说明排序结束
- 冒泡排序是稳定的
- 因为使用两个循环来解决,所以时间复杂度是O(n^2)
效果图
例子
假如需要排序的数组为 5 4 3 2 1 待排序区间为
- 待排序5 4 3 2 1 已排序区为空
- 待排序 4 3 2 1 已排序 5
- 待排序 3 2 1 已排序 4 5
- 待排序 2 1 已排序 3 4 5
- 待排序 1 已排序 2 3 4 5
- 待排序 已排序 1 2 3 4 5
代码实现
public static void bubbleSort(int []arr){ for (int i = 0; i < arr.length; i++) { for (int j = 0; j < arr.length-1-i; j++) { if (arr[j]>arr[j+1]){ int tmp=arr[j]; arr[j]=arr[j+1]; arr[j+1]=tmp; } } } }
一个算法是肯定有优化的空间的
- 当外层循环只有一个数时,就不需要比较了
- 如果数组已经是排好序的数组,就不需要继续进行循环对比了
优化后的代码
public static boolean judgeSort(int[] arr){ for (int i = 0; i < arr.length-1; i++) { if (arr[i]>arr[i+1]) { return false; } } return true; } public static void Bubblesort(int []arr){ for (int i = 0; i < arr.length-1; i++) { if (judgeSort(arr)){ break; } for (int j = 0; j < arr.length-1-i; j++) { if (arr[j]>arr[j+1]){ int tmp=arr[j]; arr[j]=arr[j+1]; arr[j+1]=tmp; } } } }
直接选择排序
选择排序是一种简单直观的排序算法,是减治算法的应用,对数据不敏感,默认是升序:
- 将数据分为两个区间,一个为无序区间,一个为有序区间,一开始无序区间为整个集合,有序区间为空,
- 每次从有无序区间中找到最小值/最大值,然后将它放在有序区间的最后/最前
- 直到无序区间为空,说明排序结束
- 直接选择排序是一个不稳定的排序算法
- 用两个循环解决,一个用来找最大/小值,一个遍历整个集合,完成所有数据的处理,所以时间复杂度是O(n^2)
代码实现
public static void selectSort(int[]arr){ //外层的循环是为了让每次循环一次,就有一个最小值放在正确的位置 for (int i = 0 ; i <arr.length-1 ; i++) { int min=i; //min指每次找到最小值的索引下标 //内存循环就是每次能找到最小的指的索引 //每次处理一个,其无序区间就-1 //每次将无序区间的开头元素跟min索引的元素互换 for (int j = i+1; j < arr.length; j++) { if (arr[j]<arr[min]){ //j对应的元素比当前最小值还小 min=j; } } swap(arr,i,min); } }
优化版本
双向直接选择排序
- 优化的核心就是,每次我们可以找到最大值和最小值,一次处理两个,将其放入正确的位置
public static void doubleSelectSort(int []arr){ int left=0;//存放最小值的索引 int right=arr.length-1;//存放最大值的索引 //left到right为无序区间的范围 //每次将最大值跟无序区间的最后一个元素互换 //将最小值跟无序区间的第一个元素互换 //处理一次无序区间就变成[left-1,right-1] //当left==right说明无序区间就剩一个元素,那么排序完成 while (left<right){ int min=left;//无序区间最小值的索引 int max=left;//无序区间最大值的索引 for (int i = left; i <=right ; i++) { if (arr[i]>arr[max]){ max=i; } if (arr[i]<arr[min]){ min=i; } } swap(arr,min,left); if (max==left){ //如果这一趟找到的最大值就是存储的无序区间的第一个元素 //会将无序区间的第一个元素根无序区间的最小值互换 //那么最大值的索引就应该变成为min的索引 max=min; } swap(arr,max,right); left++; right--; } }
堆排序
堆排序就是给你一个任意数组,如何在这个数组的基础上进行堆排序,不创建任何的空间
- 任意数组都可以看作一个完全二叉树,然后将其变为最大堆
- 不断交换堆顶元素和最后一个元素的位置,将堆顶元素不断进行siftdown操作,将最大值放在最终位置,直到数组只剩下一个元素为排序
- 堆排序是不稳定的
- 时间复杂度O(nlogn),n是因为要对n个节点进行与根节点交换的操作,logn是由于进行下沉操作的时间复杂度,是基于树操作的,所以是logn
public class HeapSort { public static void main(String[] args) { int []arr={19,27,33,26,1,29,44,87,22}; heapsort(arr); System.out.println(Arrays.toString(arr)); } 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--) { swap(arr,0,i); siftdown(arr,0,i); //将根节点进行下沉操作,因为i表示要处理的堆的长度 //每次处理好,长度都会减小,根i一样大 } } /** * 下沉操作 * @param arr * @param i * @param length */ private static void siftdown(int[] arr, int i, int length) { while (2*i+1<length){//存在左子树 int j=2*i+1; if (j+1<length&&arr[j+1]>arr[j]){ j=j+1; //存在右子树,并且右子树的值比左子树大 } if (arr[i]>arr[j]){ break; //父节点的值大于孩子节点的值,结束 }else { swap(arr,i,j); i=j; //不满足最大堆的属性,进行交换 } } } private static void swap(int[] arr, int i, int j) { int tmp=arr[i]; arr[i]=arr[j]; arr[j]=tmp; } }
直接插入排序
插入排序是一种简单直观的排序算法,是减治算法的应用,原理类似于我们打牌的过程
- 将待排序的集合看作两部分,开始的时候已排序的区间[0,1),未排序区间是[1,n)
- 每次选择无序区间的第一个元素跟有序区间的元素比较,将这个元素放到有序区间的合适位置,然后有序区间+1,无序区间-1
- 当无序区间为1的时候,就说明排序完成
- 直接插入排序是一个稳定的排序算法
- 时间复杂度是O(n^2),两个循环解决,一个循环来遍历处理集合的元素,一个循环来实现与有序区间元素比较
- 当元素较少的时候,插入排序最快
代码实现
//基本思维就是,一开始排序好的区间为[0,1),未排序区间的[1,arr.length) //每次将无序区间的第一个树在排序好的区间中找一个位置(不代表这个这个数就确定在这了)(找到一个比这个值大的值,放在它后面) //直到排序好的区间为[0,length) //比如 3 2 6 4 8 3 2 3 2 3 6 2 3 4 6 2 3 4 6 8 public static void insertionSort(int arr[]){ for (int i = 1; i <arr.length; i++) { for (int j = i; j-1>=0; j--) { if (arr[j]<arr[j-1]){ swap(arr,j,j-1); }else { break; } } } }
直接插入的特点
对于那些近乎有序的集合进行排序,时间复杂度很低,处理很高效,因为对于近乎有序的集合来说,很少有数据处理会进入内层循环,因为当处理的元素大于有序区间的最后一个元素,就说明不需要内层循环的比较处理,时间复杂度接近O(n),所以经常作为高阶算法的优化手段
- 我们发现直接插入对于近乎有序的数据处理的时间比堆排序(O(nlogn))的时间还少
直接插入的优化——折半插入排序
- 优化的核心就是我们在将无序区间的第一个元素插入有序区间的时候,我们可以将一个个比较这种笨方法用二分查找代替,因为我们插入的区间是一个有序区间
代码实现
//优化版本 //利用二叉搜索来确定我们插入的位置 //可能我们的数据是存在重复数据的,我们应该找那个地方的数据呢? //我们应该找最右边的 那个数据 public static void insertionSortBS(int arr[]){ //有序区间一开始是[0,1) 未排序区间是[1,arr.length) for (int i = 1; i <arr.length ; i++) { int val=arr[i];//在有序区间找到这个数的最右边的位置 int left=0; int right=i-1; while (left<=right){ int mid=left+(right-left)/2; if (arr[mid]>val){ right=mid-1; }else if (arr[mid]<val){ left=mid+1; }else { //相等的时候,缩小左边界 left=mid+1; } } //如果能找到 那么right就是正常找到最右边的那个边界 left=right+1 刚好是那个第一个大于val的值 //如果不能找到 有两种可能 //比区间的所有的值都大 left会大于right left=i+1 right=i 不需要交换 //比区间所有的值都小 最后right<0 left=0 全部都交换了 交换到0 for (int j = i; j-1>=left ; j--) { swap(arr,j,j-1); } } }
希尔排序
希尔排序其本质就是一个优化的插入排序,我们知道,当数据量比较小的时候,插入排序的效率比较高并且当数据近乎有序的时候插入排序的效果也比较好,希尔排序就是利用这一点,将数据集合分为很多组,然后在每个组进行插入排序,将数组变为近乎有序后将整个集合进行一次插入排序
- gap=length/2,将数组分为gap个子数组,然后在每个小数组中进行插入排序(这个分组不是真正的分组,只是逻辑上的分组,其真实的数组没有被分,并且插入排序的效果也是发生在原数组中)
- gap=gap/2 ,将经过第一轮操作的数组重新分为gap组,对每个子数组进行插入排序,重复上述操作,直到gap为0;
- 时间复杂度是O(n^1.3)到O(n^1.4)
- 希尔排序也不是稳定的,因为相同的树可能会被分到不同的子数组
代码实现
public static void shellSort(int []arr){ int gap=arr.length>>1; while (gap>=1){ insertionSortByGap(arr,gap); gap=gap>>1; } } private static void insertionSortByGap(int[] arr, int gap) { for (int i = gap; i <arr.length ; i++) { for (int j = i; j -gap>=0 ; j-=gap) { if (arr[j]>arr[j-gap]){ break; } swap(arr,j,j-gap); } } }
归并排序
原理归并排序是建立在一种建立在归并算法上一种有效的排序方法,该方法是采用分治算法的一个典型的应用
- 归,不断将原数组一分为2(只是逻辑上),实际上就是不断的分为更小的区间,直到每个子数组只剩下一个元素
- 并 不断的将相邻的两个有序的数组(逻辑上的)合并成一个更大的有序子数组,直到合并到整个数组
- 其时间复杂度是O(nlogn),n是因为要递归将数组分为n个子数组,logn是因为每次处理的方式是折半处理,所以是logn
- 这个算法是稳定的,因为是从前往后合并子数组的,先会将值相同靠前的会先被放入合并的数组
- 空间复杂度是O(n)
代码实现
/** * 归并排序 * @param arr */ public static void mergeSort(int []arr){ mergeSortInternal(arr,0,arr.length-1);//将[0,arr.length-1]的内容排序 } /** * 递归实现区间排序 * 传入区间和数组,那么这个区间就可以变为有序的 * @param arr * @param l * @param r */ private static void mergeSortInternal(int[] arr, int l, int r) { if (l>=r){ //当区间只有一个元素,肯定是有序的 return; } int mid=l+((r-l)/2); mergeSortInternal(arr,l,mid); mergeSortInternal(arr,mid+1,r); if (arr[mid]>arr[mid+1]){ merge(arr,l,mid,r); //此时说明整个数组不是有序的,需要我们合并这两个有序的子数组为一个有序的数组 } //如果第二个数组的最小值都大于第一个数组的最大值,说明,此时整个数组就是有序的,不需要处理 } /** * 将arr [l,mid] [mid+1,r]两个区间的有序数组变成[l,r]有序的数组 * @param arr * @param l * @param mid * @param r */ private static void merge(int[] arr, int l, int mid, int r) { int []aux=new int[r-l+1];//存储两个子数组的总数组 for (int i = 0; i < aux.length; i++) { aux[i]=arr[i+l]; //将arr l到r区间的数组复制到新数组 //是为了防止打乱arr这个数组,因为我们的子数组只是逻辑上的 //实际只是arr这个数组的区间罢了 } int i=l;//数组1的开始下标 int j=mid+1;//数组二的开始下标 for (int k = l; k <=r ; k++) { if (i>mid){ //说明第一个数组已经遍历完了 arr[k]=aux[j-l]; j++; }else if (j>r){ //说明第二个子数组已经遍历完了 arr[k]=aux[i-l]; i++; }else if(aux[i-l]<=aux[j-l]){ //说明两个数组中第一个子数组对应的位置小于第二个数组指向是数值 //将aux[i-l]写入arr[k] arr[k]=aux[i-l]; i++; }else { arr[k]=aux[j-l]; j++; } } }
优化点
- 对于子数组小于等于16,直接使用插入排序
- 合并的两个数组只有乱序的时候,才会进行合并成一个有序的数组
private static void mergeSortInternal(int[] arr, int l, int r) { if (r-l<=15){ //当子数组小于大小小于16,直接使用直接插入排序,速度比较快 insertionSort(arr,l,r); } int mid=l+((r-l)/2); mergeSortInternal(arr,l,mid); mergeSortInternal(arr,mid+1,r); if (arr[mid]>arr[mid+1]){ merge(arr,l,mid,r); //此时说明整个数组不是有序的,需要我们合并这两个有序的子数组为一个有序的数组 } //如果第二个数组的最小值都大于第一个数组的最大值,说明,此时整个数组就是有序的,不需要处理 } private static void insertionSort(int[] arr, int l, int r) { for (int i = l+1 ; i <=r; i++) { for (int j = i; j-1>=l ; j--) { if (arr[j]>arr[j-1]){ break; } swap(arr,j,j-1); } } }
快速排序
快速排序是20世纪最伟大的算法之一 原理:核心思想分区
- 在无序序列中选择一个基准值(通常选择最左边的值为基准值),然后遍历整个序列,每个数都和基准值进行比较,并且发生一定的交换,
- 遍历结束后使得比基准值小的数都在基准值的左边,比基准值大的数(包括等于)都在基准值的右边,然后采用分治算法的思想,分别对两个小的区间进行同样的方式处理
- 当处理的区间是等于0或者等于1的时候,就说明是有序的,不需要再排序
代码递归实现
/** * 实现数组arr的快速排序 * @param arr */ public static void quickSort(int []arr){ quickSortInternal(arr,0,arr.length-1); } /** * 将arr的[L,R]区间的元素排序 * @param arr * @param l * @param r */ private static void quickSortInternal(int[] arr, int l, int r) { //递归处理,处理好一个,就处理它的左右区间, if (l>=r){ //当区间只有一个或者为空,说明肯定有序,不需要处理 return; } int p=parttition(arr,l,r);//实现将arr最左边的元素放在合适的位置 //也就是大于等于的放在它的右边,小于它的放在它的左边,然后返回这个值所在的索引 quickSortInternal(arr,l,p-1);//处理它的左区间 quickSortInternal(arr,p+1,r);//处理它的右区间 } private static int parttition(int[] arr, int l, int r) { int val=arr[l];//默认处理最左边的元素 int x=l;//此时这个数表示小于val值的最后一个元素的索引 for (int i = l+1; i <=r ; i++) { if (arr[i]<val){ swap(arr,x+1,i); x++; //当前值小于val时候,将它于最小值区间的最后一个元素互换 } } swap(arr,x,l);//将处理的这个节点跟最小值区间的最后一个元素互换 return x; }
- 关于处理递归问题,对应这个递归函数的语义是,我们传入一个数组,然后传入一对区间值,就可以将这个数组的区间排序好,parttition可以将一个将这个数组最左边的元素放在正确的位置,但是它的左右区间并不是排序好的,所以交给这个递归函数
存在的问题
每次处理最左边的元素会存在一个非常严重的问题,就是在处理几乎有序的集合时,会大大降低效率,为什么呢?
优化
1关于每次要处理的数据的选择
- 不能武断的选择最左或者最右,三数取中法,每次去(最左值,中间值,最右值)的中间值
- 每次随机选择一个索引进行处理
2对于数据比较小的区间
- 直接使用直接插入法进行处理
/** * 实现数组arr的快速排序 * @param arr */ public static void quickSort(int []arr){ quickSortInternal(arr,0,arr.length-1); } /** * 将arr的[L,R]区间的元素排序 * @param arr * @param l * @param r */ private static void quickSortInternal(int[] arr, int l, int r) { //递归处理,处理好一个,就处理它的左右区间, // if (l>=r){ // //当区间只有一个或者为空,说明肯定有序,不需要处理 // return; // } if (r-l<=15){ insertionSort(arr,l,r); return; } int p=parttition(arr,l,r);//实现将arr最左边的元素放在合适的位置 //也就是大于等于的放在它的右边,小于它的放在它的左边,然后返回这个值所在的索引 quickSortInternal(arr,l,p-1);//处理它的左区间 quickSortInternal(arr,p+1,r);//处理它的右区间 } private static int parttition(int[] arr, int l, int r) { int randomIndex=random.nextInt(l,r+1); swap(arr,l,randomIndex); int val=arr[l];//默认处理最左边的元素 int x=l;//此时这个数表示小于val值的最后一个元素的索引 for (int i = l+1; i <=r ; i++) { if (arr[i]<val){ swap(arr,x+1,i); x++; //当前值小于val时候,将它于最小值区间的最后一个元素互换 } } swap(arr,x,l);//将处理的这个节点跟最小值区间的最后一个元素互换 return x; }
代码栈的实现
/** * 利用栈实现arr(L,R]区间的快速排序 * @param arr */ public static void quickSortNonRecursion(int arr[]){ Deque<Integer> stack=new ArrayDeque<>(); stack.push(arr.length-1); stack.push(0); while (!stack.isEmpty()){ int l=stack.pop(); int r=stack.pop(); if(l>=r){ continue; } int p=parttition(arr,l,r); //传入右区间处理 stack.push(r); stack.push(p+1); //传入左区间处理 stack.push((p-1)); stack.push(l); } } private static int parttition(int[] arr, int l, int r) { int randomIndex=random.nextInt(l,r+1); swap(arr,l,randomIndex); int val=arr[l];//默认处理最左边的元素 int x=l;//此时这个数表示小于val值的最后一个元素的索引 for (int i = l+1; i <=r ; i++) { if (arr[i]<val){ swap(arr,x+1,i); x++; //当前值小于val时候,将它于最小值区间的最后一个元素互换 } } swap(arr,x,l);//将处理的这个节点跟最小值区间的最后一个元素互换 return x; }
挖坑法Hoare实现快速排序
代码实现
public static void quickSortHoare(int arr[]){ quickSortHoareInternal(arr,0,arr.length-1); } /** * 递归实现挖坑法的快速排序 * @param arr * @param l * @param r */ private static void quickSortHoareInternal(int[] arr, int l, int r) { if (r-l<=15){ insertionSort(arr,l,r); return; } int p=partitionHoare(arr,l,r); quickSortHoareInternal(arr,l,p-1); quickSortHoareInternal(arr,p+1,r); } private static int partitionHoare(int[] arr, int l, int r) { int randomIndex=random.nextInt(l,r); swap(arr,l,randomIndex); int val=arr[l]; int i=l; int j=r; while (i<j){ while (i<j&&arr[j]>=val){ j--; } arr[i]=arr[j]; while (i<j&&arr[i]<val){ i++; } arr[j]=arr[i]; } arr[i]=val; return i; }
海量数据的排序问题
外部排序:排序过程需要在磁盘等外部存储进行的排序前提:内存只有 1G ,需要排序的数据有 100G因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
- 1. 先把文件切分成 200 份,每个 512 M
- 2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以(归并,快排,堆排)
- 3. 进行 200 路归并,同时对 200 份有序文件做归并过程,最终结果就有序了