「左程云-算法与数据结构笔记」| P5 详解桶排序以及排序内容大总结

523 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第三天,点击查看活动详情

最近在看左神的数据结构与算法,考虑到视频讲的内容和给出的资料的PDF有些出入,不方便去复习,打算出一个左神的数据结构与算法的笔记系列供大家复习,同时也可以加深自己对于这些知识的掌握,该系列以视频的集数为分隔,今天是第二篇:P5|详解桶排序以及排序内容大总结

一、快排的空间复杂度

快排的空间复杂度与时间复杂度一样,也是一个概率累加,求出的一个长期数学期望值

1、递归算法的空间复杂度

左神在课程中并没有提到空间复杂度的计算方法,因此我去搜了一下递归算法的空间复杂度的计算规则 对于递归算法,由于运行时有附加堆栈,比如: 存在递归算法 func() 第一次传参5 即执行func(5) 在函数中递归调用 func(4) 然后以此类推 func(3)、func(2)、func(1)

image.png

  • 这就是递归算法在递归的时候进行压栈而导致的空间复杂度
  • 这里给出空间复杂度的计算公式:递归的深度 × 每次压栈所需要的空间个数
    • 递归的深度:递归的深度是指栈最大的那次,也就是栈的最大大小。
    • 比如现有一个 Stack,进行操作 push(1) push(2) pop(2) push(3),栈最大深度是2,栈的深度就是2
    • 每次压栈需要的空间个数直接计算即可

2、分析

1、整体分析

  • 其实不难发现,快排的所有元素都是要压到栈中去的,直到全部压进去才会使用返回结果进行逐步弹栈
  • 因此我们可以得到:递归的深度为 N,因为所有的元素都压栈进去了,然后开始弹栈,最大深度就是开始弹栈的前一步
  • 而每次压栈所需要的空间个数就会分情况,下面分析了两种极端的情况

2、最差的情况

  • 先有一个数组 1,2,3,4,5,6,7,8,9
  • 那么如果每次做partition以最后那个数做partition,所需要的空间个数就是 N-1
  • 因此不难得出 最差的情况的空间复杂度为 O(N²)

3、最好的情况

  • 还是同样的数组,但是如果每次选的值都是中间的值,那么可以正好把数据二分,左边的数据量和右边的数据量一致,所需要的空间个数就是 logN

4、结论

  • 还是与时间复杂度一样,快排的空间复杂度使用概率论相关知识求长期数学期望,得到最终的结论为:快排的空间复杂度为O(N * logN)

二、堆

1、堆结构

其实堆结构就是用数组实现的一个完全二叉树,即在逻辑上是一个完全二叉树 完全二叉树:所有的节点要么没有子节点、要么只有左孩子、或者有左孩子和右孩子,总之就是不能只有右孩子

  • 补充一些树的基础知识:已知父节点的索引值为 i
    • 左孩子的索引值为 2i + 1
    • 右孩子的索引值为 2i + 2
    • 父节点的索引值为 (i - 1) / 2

2、分类

堆结构可以分为大根堆和小根堆: 1.大根堆:任何一个结点的值都比子节点的值要大 2.小根堆:任何一个结点的值都比子节点的值要小

3、HeapInsert

堆插入思路

  • 在插入后通过索引值的计算得到 (i - 1) / 2 的父节点的索引值
  • 与父节点的值做比较,如果比父节点的值大,就通过索引去交换二者的值
  • 然后继续比较替换之后的位置的父节点的值,以此类推,一直到交换不了为止

举例

  • 先有一个堆:7,7,5,3,6 结构如下: image.png
  • 如图我们插入一个 8,heapSize++
  • 8 的索引值为 heapSize - 1 = 5 父节点的索引值为 2 对应的值为 5
  • 比较后发现 8 比 5 大,就把索引值为 5 的和索引值为 2 的值做交换
  • 然后继续比较索引值为 0 的 7 发现还是比 7 大,继续交换
  • 此时 8 就到达了根节点,在计算不出父节点的时候就会退出循环

4、Heapify

假设现在要把堆中某个数换成一个未知的数,还要保证原本的大根堆的结构,此时就要分两种情况

情况一:换之后的数比换之前的数大

  • 因为换之后的数更大,因此对于子节点,肯定还是满足条件的
  • 只需要向上做比较,类似于 heapInsert 的过程

情况二:换之后的数比换之前的数小

  • 因为换之后的数更小,因此对于父节点,肯定还是满足条件的
  • 此时就需要向下做比较,也就是这里的 heapify

Heapify 实现思路

  • 首先利用当前位置的索引得出左孩子和右孩子的索引值
  • 然后得到左孩子和右孩子的索引对应的值,取出两个孩子的值的最大值
  • 把换之后的值与最大值做比较,如果比最大值大,那么结构就不需要变
  • 而如果最大值要大,那就交换二者的值,然后继续向下做 heapify

三、堆排序

1、代码实现

	public static void heapSort(int[] arr) {  
	   if (arr == null || arr.length < 2) {  
	      return;  
	   }  
	   for (int i = 0; i < arr.length; i++) {  
	      heapInsert(arr, i);  
	   }  
	   int size = arr.length;  
	   swap(arr, 0, --size);  
	   while (size > 0) {  
	      heapify(arr, 0, size);  
	      swap(arr, 0, --size);  
	   }  
	}  
	  
	public static void heapInsert(int[] arr, int index) {  
	   while (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 size) {  
	   int left = index * 2 + 1;  
	   while (left < size) {  
	      int largest = left + 1 < size && 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;  
	   }  
	}

2、实现思路

  • 首先遍历数组使用 heapInsert 插入堆中,按照大根堆或者小根堆排序
  • 然后弹出根节点,heapSize --,把堆中的最后一个元素放在根节点,做heapify
  • 一直循环到 heapSize == 0

3、举例

  • 数组 「1,2,4,532,623,4,7,5」
  • 首先遍历数组使用 heapInsert,得到一个大根堆 「623,532,7,1,4,2,4」
  • 然后弹出根节点 623 把尾结点 4 放在根节点做 heapify
  • 得到大根堆,然后弹出新的大根堆的根节点 532,以此类推

image.png

四、计数排序

前面的排序基本上都是基于比较的排序,这里的计数排序和基数排序并不基于比较,这种比较方式也有它的缺陷

1、实现思路

  • 先如果要对员工的年龄做排序,首先我们可以知道年龄肯定是在 0-200 区间的,我们就可以维护一个数组长度为201的数组
  • 然后有多少岁的员工,我们就在索引为哪个值上加1
  • 比如说员工年龄为:23,24,25,23,25,26,24,23
  • 那么就有
     array[23]++ array[24]++ array[25]++ array[23]++ 
     array[25]++ array[26]++ array[24]++ array[23]++
  • 就能得到 array[23]=3 array[24]=2 array[25]=2 array[26]=1
  • 即有3个23岁的,两个24岁的,两个25岁的,一个26岁的
  • 排序就是遍历数组然后输出:23,23,23,24,24,25,25,26

2、缺陷

  • 如果此时不是对员工的年龄做排序,负的范围为 -2^32 + 1 正的范围为 2^32 - 1 这个范围,再去用计数排序,很明显就太耗时了,不合适了
  • 不基于比较的排序,通常是根据数据状况而设计的排序,没有基于比较的排序适用范围广

五、基数排序

1、实现思路

  • 首先找到最大数的位数,有多少位就进行多少次循环进桶和出桶
  • 然后遍历数组依次进“桶”,此处的“桶”是队列,现进先出,进桶的规则是看某一位上的数字,第一次就是看个位上的数字,依次进各自的桶
  • 所有数都进桶后,桶从左到右进行遍历,依次出桶,按照先进先出的规则出桶
  • 然后十位上依次进桶出桶,直到遍历完最大数的位数,此时得到的数组就排序好了

2、举例

image.png

  • 对于数组 17,13,25,100,72 首先得到最大数为100,即有三位
  • 把其他的数补到与最大叔相同的位数:017,013,025,100,072
  • 然后按照个位数依次进桶:
    • 017进7桶;013进3桶;025进5桶;100进0桶;072进2桶
  • 然后从左到右倒出桶的数据:100,072,013,025,017
  • 然后按照十位数依次进桶:
    • 100进0桶;072进7桶;013进1桶;025进2桶;017进1桶
  • 然后从左到右倒出桶中的数据(注意:按照队列的结构,下面的先出):100,013,017,025,072
  • 然后按照百位数依次进桶:
    • 100进1桶;013,017,025,072进0桶
  • 然后从左到右倒出桶中数据:013,017,025,072,100
  • 排序结束

3、代码实现

	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 void radixSort(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 (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];  
	         count[j]--;  
	      }  
	      for (i = begin, j = 0; i <= end; i++, j++) {  
	         arr[i] = bucket[j];  
	      }  
	   }  
	}  
	  
	public static int getDigit(int x, int d) {  
	   return ((x / ((int) Math.pow(10, d - 1))) % 10);  
	}