排序理论
汇总
排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
选择排序 | O(n^2) | O(1) | 无 |
冒泡排序 | O(n^2) | O(1) | 有 |
插入排序 | O(n^2)(常数时间极低) | O(1) | 有 |
归并排序 | O(n*logN) | O(n) | 有 |
快速排序 | O(n*logN) | O(logN) | 无 |
堆排序 | O(n*logN) | O(1) | 无 |
总结:
- 优先使用快速排序(常数时间在三个时间复杂度为O(n*logN)的排序方法中最小)
- 需要稳定性则使用归并排序
- 空间复杂度有要求则使用堆排序
- 工程上排序的改进(综合排序)
- 充分利用时间复杂度为O(n*logN)和O(n^2)排序各自的优势(快速排序+插入排序)
- 稳定点的考虑
- 基础数据类型使用快速排序
- 非基础数据类型使用归并排序
- 选择排序和冒泡排序时间复杂度一直是O(n2),插入排序时间复杂度最差情况是O(n2)
- 哈希表的增删改查操作的时间都是常数级别O(1),但是这个常数时间比较大
- 若哈希表中存储的是基础类型,内部按值传递,内存占用即存储数据的大小
- 若哈希表中存储的不是基础类型,内部按引用传递,内存占用的是该数据内存地址的大小
选择排序
i=0,从i=1开始后面找最小的跟i=0换,不断循环,直到i遍历结束(第一层for循环每执行完一次,第一个位置就确认了)
时间复杂度:O(n*n)
代码:
public void selectionSort(int[] arr){
if(arr==null||arr.length<2){
return;
}
for(int i = 0;i<arr.length-1;i++){
int minIndex=i;
for(int j =i+1;j<arr.length;j++){
minIndex=arr[j]<arr[minIndex]?j:minIndex;
}
swap(arr,i,minIndex);
}
}
public void swap(int [] arr,int i,int minIndex){
int temp=arr[i];
arr[i]=arr[minIndex];
arr[minIndex]=temp;
}
选择排序为什么不稳定?
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n - 1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等 的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
冒泡排序
i=0,第一层for循环每执行完一次,最后一位的位置就确认了
等于的情况不交换,冒泡排序即为稳定的
public void BubbleSort(int [] arr){
if(arr==null||arr.length<2){
return;
}
for(int i=arr.length-1;i>0;i--){
for(int j=0;j<i;j++){
if(arr[j]>arr[j+1]){
swap(arr,j,j+1);
}
}
}
}
public void swap(int [] arr,int i,int j){
arr[i]=arr[i]^arr[j];
arr[j]=arr[i]^arr[j];
arr[i]=arr[i]^arr[j];
}
补充题目:找出数组中两个为奇数个数的数
思路:两个不一样的数异或,即二进制中至少有一位不一样异或的结果是1,取出最右边为1的那位数(这个数为除了最右边的1之外其他位都为0的数),根据所有数中这个位置上的数是否为1(即异或结果是否为0)可以将两个数区分开来,再将其中一堆数进行全部异或得到其中一个数,从而得到两个数
public int[] getTwoNumbers(int [] arr){
if(arr.length<2||arr==null){
return null;
}
if(arr.length==2){
return arr;
}
int eor=0;
for(int number:arr){
eor^=number;//找到一个奇数个数的数的解法
}
int rightOne=eor&(~eor+1);//找到eor二进制中最右边为1的数
int onlyOne=0;
for(int number:arr){
if(number&rightOne==0){
onlyOne^=number;
}
}
int[] result={onlyOne,eor^onlyOne};
return result;
}
插入排序
-
插入排序的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束
-
如果要排序的数据已经是有序的,我们并不需要搬移任何数据。如果我们从尾到头在有序数据组里面查找插入位置,每次只需要比较一个数据就能确定插入的位置。所以这种情况下,最好情况时间复杂度为 O(n)。
public void insertionSort(int[] arr){
if(arr==null||arr.length<2){
return;
}
for(int i=1;i<arr.length;i++){
for(int j=i-1;j>=0&&arr[j]>arr[j+1];j--){
swap(arr,j,j+1);
}
}
}
public void swap(int [] arr,int i,int j){
arr[i]=arr[i]^arr[j];
arr[j]=arr[i]^arr[j];
arr[i]=arr[i]^arr[j];
}
另一种代码实现
public void int[] ChaRu(int[] arr) {
for (int i = 1; i < arr.length; i++) {
//curr表示要插入的数
int curr = arr[i];
//temp表示要插入数之前的一个数
int temp = i - 1;
while (temp >= 0 && curr < arr[temp]) {
arr[temp + 1] = arr[temp];
temp--;
}
arr[temp + 1] = curr;
}
}
补充:递归时间复杂度计算公式:master公式
满足如上公式的程序都可以根据master公式计算时间复杂度:
- log(b,a) > d :时间复杂度为O(N^log(b,a))
- log(b,a) = d :时间复杂度为O(N^d * logN)
- log(b,a) < d :时间复杂度为O(N^d)
二分法
-
有序是二分的基础?
-
通过二分法在一个数组中查找一个数是否存在
public boolean exist(int[] arr,int num){ if(arr==null||arr.length==0){ return false; } Arrays.sort(arr);//排序 int L=0; int R=arr.length-1; int mid=0; while(L<R){ mid=L+((R-L)>>1);//小技巧:要取两个数的中点直接使用(L+R)/2不合理,可能会溢出,通过代码中的方式不会溢出,L不溢出,R不溢出,R-L不溢出 if(arr[mid]==num){ return true; }else if(arr[mid]>num){ R=mid-1; }else{ L=mid+1; } } return arr[L]==num;//如果全部遍历完循环结束的话只剩一个数 }
-
在一个有序数组arr中,找到满足>=value的最左位置
public int nearestIndex(int[]arr,int value){ if(arr==null||arr.length==0){ return false; } int L=0; int R=arr.length-1; int mid=0; int index=-1; while(L<R){ mid=L+((R-L)>>1); if(arr[mid]>=value){ R=mid-1; index=mid;//key point }else{ L=mid+1; } } return index; }
归并排序
递归:通过左右两边都排好序,最后merge让整体有序
public void mergeSort(int[] arr){
if(arr==null||arr.length<2){
return;
}
mergeSort(arr,0,arr.length-1);
}
public void mergeSort(int[] arr,int l,int r){
if(l==r){
return;
}
int mid=l+((r-l)>>1);
mergeSort(arr,l,mid-1);
mergeSort(arr,mid+1,r);
merge(arr,l,mid,r);
}
//治
public void merge(int[] arr,int l,int mid,int r){
int[] help=new int[r-l+1];
int i=0;
int p1=l;
int p2=mid+1;
//通过三个while+一个for循环拷贝全部
while(p1<=mid&&p2<=r){
help[i++]=arr[p1]<=arr[p2]?arr[p1++]:arr[p2++];//这里相等时应先拷贝左组,保证稳定性
}
while(p1<=mid){
help[i++]=arr[p1++];
}
while(p2<=r){
help[i++]=arr[p2++];
}
for(int i=0;i<help.length;i++){
arr[l+i]=help[i];
}
}
- 根据master公式:a=2,b=2,d=1得出时间复杂度为 O(n*logN)
- 时间复杂度相比冒泡那三个降低,原因在于没有浪费每一次比较的过程
- 额外空间复杂度为O(n),help数组
小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
[1,3,4,2,5]
1+1+3+1+1+3+4+2=16
反着想:右边比1大的有四个 1x4+3x2+4x1+2x1=16
可以用两个for循环实现,时间复杂度为O(N^2)
使用归并排序降低时间复杂度
- 分完以后开始治,归并排序的治就是merge的过程,首先对1和3进行merge,在此过程中产生一个小和1;然后将1、3和4进行merge,在此过程中产生小和1、3;然后2和5进行merge,产生小和2;最后将1、3、4和2、5进行一次merge,1比2小,所以一共产生n个1的小和,这个n就是当前右边的数的个数,因为右边有两个数2和5,所以产生2个1的小和,然后将1填入辅助数组,继续比较3和2,2比3小,但是2是右边的数,所以不算小和,然后比较3和5,3比5小,所以产生n个3的小和,因为右侧只有一个数,所以就只产生1个3的小和,同样的,产生1个4的小和
- 这道题换个角度来想,题目要求的是每个数左边有哪些数比自己小,其实不就是右边有多少个数比自己大,那么产生的小和就是当前值乘以多少个吗?还是以上面的样例举例,1右边有4个比1大的数,所以产生小和1x4;3右边有2个比3大的数,所以产生小和3x2;4右边有一个比4大的数,所以产生小和4x1;2右边有一个比2大的数,所以产生小和为2x1;5右边也没有比5大的数,所以产生小和5x0
public int smallSum(int[] arr){
if(arr==null||arr.length<2){
return 0;
}
return mergeSort(arr,0,arr.length-1);
}
public int mergeSort(int[] arr,int l,int r){
if(l==r){
return 0;
}
int mid=l+((r-l)>>1);
return mergeSort(arr,l,mid)+mergeSort(arr,mid+1,r)+merge(arr,l,mid,r);
}
public int merge(int[] arr,int l,int mid,int r){
int[] help=new int[r-l+1];
int i=0;
int p1=l;
int p2=mid+1;
int res=0;
while(p1<=mid&&p2<=r){
res+=arr[p1]<arr[p2]?arr[p1]*(r-p2+1):0;
help[i++]=arr[p1]<arr[p2]?arr[p1++]:arr[p2++];//这里相等时先拷贝右组(与经典merge不同)
}
while(p1<=mid){
help[i++]=arr[p1++];
}
while(p2<=r){
help[i++]=arr[p2++];
}
for(int i=0;i<help.length;i++){
arr[l+i]=help[i];
}
return res;
}
逆序对问题
对于给定的一段正整数序列,逆序对就是序列中ai>aj,且i<j的有序对;注意序列中可能有重复数字,并分析算法的时间性能。(如果左边的数比右边的数大,则构成一个逆序对)
例如:有6个数字,分别是5,4,2,6,3,1,则逆序对数目是11。
同样可以使用两层for循环暴力解,使用归并排序可以降低时间复杂度
public int printAll(int[] arr){
if(arr==null||arr.length<2){
return 0;
}
return mergeSort(arr,0,arr.length-1);
}
public int mergeSort(int[] arr,int l,int r){
if(l==r){
return 0;
}
int mid=l+((r-l)>>1);
return mergeSort(arr,l,mid)+mergeSort(arr,mid+1,r)+merge(arr,l,mid,r);
}
public int merge(int[] arr,int l,int mid,int r){
int[] help=new int[r-l+1];
int p1=l;
int p2=mid+1;
int result=0;
int i=0;
while(p1<=mid&&p2<=r){
result+=arr[p1]>arr[p2]?(r-p2+1):0;
help[i++]=arr[p1]>arr[p2]?arr[p1++]:arr[p2++];
}
while(p1<=mid){
help[i++]=arr[p1++];
}
while(p2<=r){
help[i++]=arr[p2++];
}
for(int j=0;j<help.length;j++){
arr[l+j]=help[j];
}
return result;
}
[快速排序]图文详解JAVA实现快速排序_java_脚本之家 (jb51.net)
荷兰国旗问题
一个数组arr,一个数num,小于等于num的数放在数组的左边,大于num的数放在数组的右边(不要求排序)
要求时间复杂度O(N),空间复杂度O(1)
public void patition(int[] arr,int l,int r,int num){
int less=l-1;
int more=r+1;
while(l<more){
if(arr[l]<num){
swap(arr,++less,l++);
}else if(arr[l]>num){
swap(arr,--more,l);
}else{
l++;
}
}
}
public void swap(int[] arr,int i,int j){
arr[i]=arr[i]^arr[j];
arr[j]=arr[i]^arr[j];
arr[i]=arr[i]^arr[j];
}
快速排序
快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)。其实快速排序是基于一种叫做“二分”的思想。
随机选一个数与最后一个数进行交换,拿最后一个数作划分,每一种情况都是1/N的概率,求数学期望,快排3.0的时间复杂度为O(logN*N)[左神算法课]
public void quickSort(int[] arr) {
if (arr == null||arr.length < 2) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
public void quickSort(int[] arr,int l,int r){
if(l<r){
swap(arr,l+(int)(Math.random()*(r-l+1)),r);
int[] p=patition(arr,l,r);
quickSort(arr,l,p[0]-1);
quickSort(arr,p[1]+1,r);
}
}
//patition返回结果是排好序的部分的b
public int[] patition(int[] arr,int l,int r){
int less=l-1;
int more=r;//注意这里和荷兰国旗问题的不同
while(l<more){
if(arr[l]<arr[r]){
swap(arr,++less,l++);
}else if(arr[l]>arr[r]){
swap(arr,--more,l);
}else{
l++;
}
}
swap(arr,more,r);//最后一个数没参与进来
return new int[]{less+1,more};
}
public void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
快排额外空间复杂度是O(logN)(概率累加),最差情况是O(N)
堆排序
堆:可以看作完全二叉树(满二叉树或者最下一层从左到右是满的),堆是特殊的完全二叉树,堆分为大根堆和小根堆
优先级队列就是堆结构
大根堆:每一颗子树的最大的数是头节点的值
因此可以把一个数组想象成一颗完全二叉树(连续的顺序从0开始依次排)
i位置(下标)的左孩子是2*i+1
,右孩子是2*i+2
,父节点是(i-1)/2
一组连续的数可以变成大根堆(heapinsert方法)——>如何得到一组连续的数中最大的数**(取头节点)**并且去掉该数保证剩下的连续的数为大根堆(连续的数的数量用heapsize记录,也可以理解为控制数组范围):把最后一个数的数值覆盖掉头节点并且该数值与自己的左孩子和右孩子的较大值进行比较,如果小于则交换,直到没有左孩子和右孩子或者左右孩子的较大值比该值小(heapify过程)
如果i位置的数值发生了改变,怎么调整?跟原先的值相比变大了,则heapinsert,变小了则heapify,时间复杂度为logN级别(完全二叉树有n个数,高度是logN级别)
堆结构两个重要操作:heapinsert和heapify
堆排序:先将数组进行操作转化成大根堆,将根节点与最后一个数交换(即第一个数和最后一个数进行交换),heapSize–,然后进行heapify操作,继续把第一个数和最后一个数进行交换,heapSize–,heapify,直到heapSize=0,即排好了序
代码:
public void heapSort(int[] arr){
if(arr==null||arr.length<2){
return;
}
for(int i=0;i<arr.length;i++){//O(N)
heapInsert(arr,i);//O(logN)
}
int heapSize=arr.length;
swap(arr,0,--heapSize);
while(heapSize>0){//O(N)
heapify(arr,0,heapSize);//O(logN)
swap(arr,0,--heapSize);//O(1)
}
}
public void heapInsert(int[] arr,int index){
while(arr[index]>arr[(index-1)/2]){//去到头节点的时候-1/2等于0,两者恒等,退出循环,一句代码包含了两个退出条件
swap(arr,index,(index-1)/2);
index=(index-1)/2;
}
}
public void heapify(int[] arr,int index ,int heapSize){
int left=2*index+1;
while(left<heapSize){//左孩子小的话右孩子肯定也小
int largest=left+1<heapSize&&arr[left+1]<arr[left]?left+1:left;//取左右孩子的较大值,当右节点不存在时,即只存在左节点,取左节点
largest=arr[largest]>arr[index]?largest:index;//比较取较大值
if(largest==index){
break;
}
swap(arr,largest,index);
index=largest;
left=index*2+1;
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
堆排序的时间复杂度是O(logN*N),空间复杂度是O(1)
拓展题:已知一个几乎有序的数组(与排好序的相比移动距离不超过k,k较小),选择一个合适的排序算法针对这个数据进行排序
比如k=6,遍历数组前七个数,形成小根堆,0位置上的数就可以确认了,弹出,并将索引为7的数加入,形成小根堆,循环
时间复杂度为O(logk*N)
java语言默认提供的堆结构(小根堆)(优先级队列)
可以把java提供的堆结构理解成黑盒,它能干的只有给它一个数它变成堆结构,想要一个数它弹出
不能对它其中一个数进行修改并指望他重新调整堆结构(实现的代价高)
如果需要实现上述需求,可以自己写堆进行操作
因此有些题目需要自己手写堆
PriorityQueue<Integer> heap=new PriorityQueue<>();
heap.add(8);
heap.poll();
代码:
public void sortedArrDistanceLessK(int[] arr,int k){
PriorityQueue<Integer> heap=new PriorityQueue<>();
int index=0;
for(;index<Math.min(arr.length,k);index++){//取两者较小值保证k的有效
heap.add(arr[index]);
}
int i=0;
for(;index<arr.length;index++,i++){
heap.add(arr[index]);
arr[i]=heap.poll();
}
while(!heap.isEmpty()){
arr[i++]=heap.poll();
}
}
比较器
比较器实质就是重载,比较运算符,可以很好地应用在特殊标准的排序上,可以很好地根据特殊标准排序的结构上
以上的排序都是基于比较的排序,接下来的是不基于比较的排序
不基于比较的排序都是基于数据状况的排序,没有基于比较的排序那么广的应用范围,需要根据数据状况定制
不基于比较的排序
计数排序
准备一个包含数组中数的范围大小的数组并记录词频,最后复原数组给数组的下标赋予意义(词频数组)
时间复杂度O(n)
缺点:数组中范围不确定或数的范围很大时,无法应用该方法进行排序
//only for 0-200 value
public void countSort(int[] arr){
if(arr==null||arr.length<2){
return;
}
int max=Integer.MIN_VALUE;
for(int i=0;i<arr.length;i++){
max=Math.max(max,arr[i]);
}
int[] bucket=new int[max+1];
for(int i=0;i<arr.length;i++){
bucket[arr[i]]++;
}
int i=0;
for(int j=0;j<bucket.length;j++){
while(bucket[j]-->0){
arr[i++]=j;
}
}
}
基数排序
依然跟数据状况有关,排序的对象需要有进制
比如十进制,看最大的数有几位,其他小的数前面补0补齐
准备10个桶(队列),从个位数开始,分别根据个位数上的数字倒进桶里,全部倒出(从左到右),根据十位上的数进桶,再倒出,循环,最后一次倒出桶即排好序
比计数排序好(如果对象可以被看作十进制,则就是十个队列,再没其他东西)
public void radixSort(int[] arr){
if(arr==null||arr.length<2){
return;
}
radixSort(arr,0,arr.length-1,maxbits(arr));
}
//获得最大的数有几位
public int maxbits(int[] arr){
int max=Integer.MIN_VALUE;
for(int i=0;i<arr.length;i++){
max=Math.max(max,arr[i]);
}
int res=0;
while(max!=0){
res++;
max/=10;
}
return res;
}
public void redixSort(int[] arr,int begin,int end,int digit){
//用于辅助数组
final int radix=10;
int i=0,j=0;
int[] bucket=new int[end-begin+1];
for(int d=1;d<=digit;d++){//一共有几位数就操作几次
//记录词频
int[] count=new int[radix];
for(int i=begin;i<=end;i++){
j=getDigit(arr[i],d);
count[j]++;
}
//词频前缀和?
for(i=1;i<radix;i++){
count[i]=count[i]+count[i-1];
}
//从右往左看,模拟先进先出的队列
for(i=end;i>=begin;i--){
j=getDigit(arr[i],d);
bucket[count[j]-1]=arr[i];//co
count[j]--;
}
for(i=begin,j=0;i<=end;i++,j++){
arr[i]=bucket[j];
}
}
}
public int getDigit(int x,int d){
return ((x/(Math.pow(10,d-1)))%10);//d-1
}
排序算法的稳定性
同样的个体之间,如果不因为排序而改变相对次序,就说这个排序具有稳定性,否则则没有
目前没有找到时间复杂度O(logN*N),额外空间复杂度O(1),又稳定的排序
目前的算法各有优缺,都是在时间复杂度和空间复杂度之间找平衡
有序表TreeSet、TreeMap
在使用层面上可以理解为一种集合结构
有序表把key按照顺序组织起来,而哈希表完全不组织
红黑树、AVL树、size-balance-tree和跳表都属于有序表结构,只是底层具体实现不同
放入有序表的东西,如果不是基础数据类型,必须提供比较器,内部按照引用传递
有序表的操作时间复杂度都是O(logN)