【3】详解桶排序及排序内容大总结

目录

一、快排额外空间复杂度

二、堆排序

1.完全二叉树

2.堆排序的实现 

3.堆排序拓展题目

三.比较器的使用

四.桶排序

1.计数排序 

2.基数排序

五.排序算法的稳定性及其汇总

1.选择排序

2.冒泡排序

3.插入排序

 4.归并排序

5.快速排序

6.堆排序

7.排序比较

8.常见的坑 

9.工程上对排序进行改进


一、快排额外空间复杂度

在最差的情况下,每次都只有单侧区间,这样栈的深度就是O(N) => 额外空间O(N) 

在比较好的情况,取的划分值恰好在中点附近,这样就是一棵二叉树,栈的深度是O(NlogN) => 额外空间O(NlogN)        (在一个节点的左枝递归结束后释放的空间可以被右枝复用)

 快排额外空间复杂度与划分值的选择有关,而划分值的选择是概率事件。在等概率的情况下,数学上的累加期望是O(NlogN) => 一般认为快排的额外(栈)空间为O(NlogN)

二、堆排序

1.完全二叉树

堆在逻辑结构上其实是一棵完全二叉树

 完全二叉树:每层都是满的 or 在不满的最后一层也是从左到右变满的

 连续的数组 可以想象成一棵完全二叉树

(下标从0开始)

位置i的左孩子2*i+1    右孩子2*i+2  父亲(i-1)/2

(下标从1开始)

位置i的左孩子2*i  右孩子2*i+1  父亲i/2

2.堆排序的实现 

大顶堆:每一棵(子)树的根是该树的最大值  

以6为根的树最大值是6

以5为根的子树的最大值时5

……

小顶堆同理

 堆排序实现如下:

	public static 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 static void heapInsert(int[] arr, int index) {
		while(index-1>0&&arr[index] > arr[(index-1)]/2) {
			swap(arr, index, (index-1)/2);
			index=(index-1)/2;
		}
	}
	
	//堆调整
	public static void heapify(int[] arr, int index, int heapSize) {
		int left=index*2+1; //左孩子
		while(left<heapSize) { //下方还有孩子
			//两个孩子中,谁的值打,把下标给largest
			int largest=left+1<heapSize&&arr[left+1]>arr[left]?left+1:left;
			//父和孩子之间,谁的值大,白下标给largest
			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 x, int y) {
		int tmp=arr[x];
		arr[x]=arr[y];
		arr[y]=tmp;
	}

heapInsert方法是处理加入数字放置位置

heapify方法可以从任意位置index开始进行堆调整

上述两个方法都是O(logN)级别的操作

3.堆排序拓展题目

已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。

 只需要维护一个大小为6(k)的小顶堆。

一开始,先将0-5的数全部加入小顶堆,然后选出最小的放在0位置,此时该位置的数值一定是最小的(应该放在0位置的数值一定在[0,5]这些位置中)

随后,加入8位置的数(替换掉堆顶已经用过的最小值)并调整堆,然后取出最小值应该放在1位置上……下面同理

最后,数组所有数值取完后,只需要将小根堆的数不断堆调并取出堆顶元素即可。

时间复杂度O(N*logK)  => 如果k很小,这几乎是一个O(N)的算法

	public void sortArrayDistanceLessK(int[] arr, int k) {
		//默认小根堆
		PriorityQueue<Integer> heap=new PriorityQueue<>();
		int index=0;
		for(;index<=Math.min(arr.length, k);index++) {
			heap.add(arr[index]);
		}
		int i=0;
		for(;index<arr.length;i++,index++) {
			heap.add(arr[index]);
			arr[i]=heap.poll();
		}
		while(!heap.isEmpty()) {
			arr[i++]=heap.poll();
		}
		
	}

小顶堆也可以不用自己实现,可以直接调用自带的优先队列,优先队列的本质是堆。堆结构扩容均摊下来是O(logN)代价。

简单使用堆排序可以直接使用优先队列。但是如果要灵活修改调整或完成某个具体功能需要自己手写堆。

三.比较器的使用

  • 比较器的实质是重载比较运算符
  • 比较器可以很好的应用在特殊标准的排序上
  • 比较器可以很好应用在根据特殊标准排序的结构上
	public static class AComp implements Comparator<Integer>{
		public int compare(Integer arg0, Integer arg1) {
			return arg1-arg0;
		}
	}

    public static void main(String[] args) {
		
		Integer[] arr= {5,4,3,2,7,9,1,0};
		
		Arrays.sort(arr, new AComp());
		
		for(int i=0;i<arr.length;i++) {
			System.out.println(arr[i]);
		}
    }

在java中的比较器,在C++中其实就是重载运算符。 

	public static class AComp implements Comparator<Integer>{
		public int compare(Integer arg0, Integer arg1) {
			return arg1-arg0;
		}
	}
	
	public static void main(String[] args) {
		
		PriorityQueue<Integer> heap=new PriorityQueue<>(new AComp());
		
		heap.add(8);
		heap.add(4);
		heap.add(4);
		heap.add(9);
		heap.add(10);
		heap.add(3);
		
		while(!heap.isEmpty()) {
			System.out.println(heap.poll());
		}
		
	}

比较器可以用在堆排序中,优先队列默认的是小顶堆。如果如上述代码按照降序规定,就可以得到大顶堆。 

四.桶排序

冒泡、选择、插入、归并、快速、堆排序都是基于比较的排序,而桶排序是不基于比较的排序。

1.计数排序 

上述计数排序,统计每个数出现的次数,最后再从左到右遍历词频数组将每个数依次取出词频个。上述这种方法仅适用数值范围较小的情况,如果数据范围很大则不适用。

2.基数排序

为什么桶排序可以区分百位?十位?个位?

百位是最后排序的,其优先级最高;十位是倒数第二排序的,其优先级次高;而个位是最早排序的,其优先级最低。因此,可以很好区分每个位。

 该方法比计数排序好,桶的个数不会太多,如果是K进制数,则只需要准备K个桶即可。

只要不是基于比较的排序,都和数据高度相关,比如这里桶的数量需要根据数据的进制确定,再比如计数排序的词频数组大小和数据的大小有关。

先按照个位将数组的数依次放进桶内,随后从0号桶将这些数依次倒出来 

第二次按照十位数字进桶,然后从0号桶依次倒出来 

第三次按照百位数字进桶,然后导出来就有序了。 

count是一个前缀和数组,用于统计小于等于当前(数值d位)的数有多少个(实现d位不同数字的分片,将d位相同的分到了一片),相当于一次入桶。

随后从右向左遍历数组每个数字,然后根据(数值d位)的count数字来确定出桶后应该处在的位置。

从后向前遍历是可以让后进的数值比较靠后(count值还比较大),从而保证出桶满足先进先出,后进后出的原则。

	public static void radixSort(int[] arr) {
		if(arr==null||arr.length<2)return;
		radixSort(arr,0,arr.length-1,maxbits(arr));
	}
	
	//返回数组中最大值有几个位
	public static 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 static int getDigit(int x, int d) {
		return ((x/((int)Math.pow(10, d-1)))%10);
	}
	

	public static void radixSort(int[] arr, int L, int R, int digit) {
		final int radix=10;
		int i=0, j=0;
		//有多少个数准备多少个辅助空间
		int[] bucket=new int[R-L+1]; 
		for(int d=1;d<=digit;d++) { //有多少个位就进出几次
			//10个空间
			//count[0] 当前位(d位)是0的数字有多少个
			//count[1] 当前位(d位)是(0和1)的数字有多少个
			//count[2] 当前位(d位)是(0、1和2)的数字有多少个
			//count[i] 当前位(d位)是(0~i)的数字有多少个
			int[] count=new int[radix]; //count[0..9]
			for(i=L;i<=R;i++) {
				j=getDigit(arr[i],d);
				count[j]++;
			}
			for(i=1;i<radix;i++) {
				count[i]=count[i-1]+count[i];
			}
			for(i=R;i>=L;i--) {
				j=getDigit(arr[i],d);
				bucket[count[j]-1]=arr[i];
				count[j]--;
			}
			for(i=L,j=0;i<=R;i++,j++) {
				arr[i]=bucket[j];
			}
			
		}
	}

五.排序算法的稳定性及其汇总

稳定性:同样值之间,不因为排序而改变相对次序。

不具备稳定性的排序:选择排序、快速排序、堆排序

具备稳定性的排序:冒泡排序、插入排序、归并排序、一切桶思想下的排序

目前没有找到时间复杂度O(NlogN),额外空间复杂度O(1),又稳定的排序

如上图,标号为1的数字1排序前后都在标号2的数字1前面,其他相同数字之间的相对次序也没有改变,那么这个排序算法就是稳定的。(排序算法的稳定性可以保留相对次序) 

稳定性在实际生活中可能比较有用,比如我可以对购买的商品按照价格排序一次,然后再按照好评率排一次,这样在最上面的就是一些物美价廉的商品。

1.选择排序

如上图,在0~N-1找到最小的1和0位置的3交换,这一交换就破坏了3之间的相对次序(不具备稳定性)

2.冒泡排序

冒泡排序可以保持稳定性,比如数值5在冒泡过程中遇到了5,可以不用交换,直接用后边的那个5继续往后冒泡。(具有稳定性)

3.插入排序

如上图插入2的时候,遇到相同元素是就直接停下,可以不发生交换。(具有稳定性) 

 4.归并排序

 

 归并排序遇到相等的数时,可以每次都让左边的数先进,这样一样可以保证最终相同数之间的相对次序。(具有稳定性)

5.快速排序

 如上图所示,当指针指到3的时候需要移入<=区域,这时就是发生3和最前的6发生交换,这时就破坏的相同数之间的相对次序。(不具有稳定性)

6.堆排序

如上图,插入数字6的时候,发现6比4大,因此需要将6与它的父亲4进行交换。看数组可以发现,这样一交换,数组中两个4的相对次序就发生了改变,破坏了稳定性。(不具有稳定性) 

7.排序比较

O(N^2)中,冒泡、插入优于选择。

O(NlogN)中,归并用于需要稳定的情况、堆排用于省时省空间、快排可以用于省时。

8.常见的坑 

  1. 归并排序的额外空间复杂度可以变成O(1),但是肥肠难,不需要掌握,有兴趣可以搜“归并排序 内部缓存法”
  2. “原地归并排序”的帖子都是垃圾,会让归并排序的时间复杂度变成O(N^2)
  3. 快速排序可以做到稳定性问题,但是肥肠难,不需要掌握,可以搜“01 stable sort”
  4. 所有的改进都不重要,因为目前没有找到时间复杂度O(NlogN),额外空间复杂度O(1),又稳定的排序
  5. 有一道题目,是奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变,碰到这个问题,可以怼面试官(把一种情况放在左边,把另一种情况放在右边,这是经典的01基准的paratition(快排1.0),很难做到保持相对次序)

 9.工程上对排序进行改进

利用各自排序的优势进行拼装。

 充分利用O(NlogN)和O(N^2)各自的优势,大调度上用O(NlogN),小样本利用O(N^2)(在小样本上的常数比较小)

	public static void quickSort(int[] arr, int L, int R) {
		if(l==r)return;
		if(l>r-60) {
			在arr[l..r]插入排序
			O(N^2)小样本量的时候,跑的快
			return
		}
		swap(arr, L+(int)(Math.random())*(R-L+1),R);
		int[] p=partition(arr, L, R);
		quickSort(arr, L, p[0]-1); //《区
		quickSort(arr, p[1]+1, R); //>区
	}

  • 13
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

看未来捏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值