《计算之魂》读书笔记 04


1.4 关于排序的讨论


  • 排序算法在计算机算法中占有重要位置

  • 根据时间复杂度,排序算法可分为两类:
    • 复杂度为 O ( n 2 ) \bm{O(n^2)} O(n2):大多较直观
    • 复杂度为 O ( n l o g n ) \bm{O(nlogn)} O(nlogn)执行效率高

  • 要理解排序算法,就要先掌握 递归和分治


回顾:通过学习前两小节内容,我们认识了 5 种直观和高效的排序算法


【1.4.3】针对特殊情况,我们是否还有更好的答案?

  • 使用一种排序算法难以兼顾多维需求,因此今天排序算法的改进大多是 混合排序算法

  • 内省排序(Introsort):

    • 快速排序 + 堆排序 + 插入排序

    • 在大多标准函数库(STL)的排序函数中使用

    • 结合三种算法的优点,最坏情况下的运行时间为 O ( n l o g n ) O(nlogn) O(nlogn)

    • 动态演示from YouTube):

      内省排序

    • 伪代码:

      sort(A : array):
      	# 递归深度限制
          depthLimit = 2xfloor(log(length(A)))
          introsort(A, depthLimit)
      
      introsort(A, depthLimit):
          n = length(A)
          if n <= 16:
          	# 数据规模小时(元素数量低于某个阈值),切换到插入排序
              insertionSort(A)
          if depthLimit == 0:
          	# 当递归深度超过一定限制时,切换到堆排序
              heapsort(A)
          else:
              # 规模适中,切换到快速排序,p 即 pivot
              p = partition(A)  
              introsort(A[0 : p - 1], depthLimit - 1)
              introsort(A[p + 1 : n], depthLimit - 1)
      
    • 这里,快速排序的 pivot 选取使用了 “三点中值法”,即每次选取的 pivot,是 这三个位置值的中位数,以避免最坏情况发生

    • 实现示例

      import java.io.IOException;
      
      public class IntroSort {
          // the actual data that has to be sorted
          private int a[];
          // the number of elements in the data
          private int n;
          // Constructor to initialize the size of the data
          IntroSort(int n) {
              a = new int[n];
              this.n = 0;
          }
      
          // The utility function to insert the data
          private void dataAppend(int temp) {
              a[n] = temp;
              n++;
          }
      
          private void swap(int i, int j) {
              int temp = a[i];
              a[i] = a[j];
              a[j] = temp;
          }
      
          // To maxHeap a subtree rooted with node i which is an index in a[]. heapN is size of heap
          private void maxHeap(int i, int heapN, int begin) {
              int temp = a[begin + i - 1];
              int child;
      
              while (i <= heapN / 2) {
                  child = 2 * i;
                  if (child < heapN
                          && a[begin + child - 1] < a[begin + child])
                      child++;
                  if (temp >= a[begin + child - 1])
                      break;
                  a[begin + i - 1] = a[begin + child - 1];
                  i = child;
              }
              a[begin + i - 1] = temp;
          }
      
          // Function to build the heap (rearranging the array)
          private void heapify(int begin, int end, int heapN) {
              for (int i = (heapN) / 2; i >= 1; i--)
                  maxHeap(i, heapN, begin);
          }
      
          // main function to do heapsort
          private void heapSort(int begin, int end) {
              int heapN = end - begin;
      
              // Build heap (rearrange array)
              this.heapify(begin, end, heapN);
      
              // One by one extract an element from heap
              for (int i = heapN; i >= 1; i--) {
      
                  // Move current root to end
                  swap(begin, begin + i);
      
                  // call maxHeap() on the reduced heap
                  maxHeap(1, i, begin);
              }
          }
      
          // function that implements insertion sort
          private void insertionSort(int left, int right) {
      
              for (int i = left; i <= right; i++) {
                  int key = a[i];
                  int j = i;
      
                  // Move elements of arr[0..i-1]
                  while (j > left && a[j - 1] > key) {
                      a[j] = a[j - 1];
                      j--;
                  }
                  a[j] = key;
              }
          }
      
          // Function for finding the median of the three elements
          private int findPivot(int a1, int b1, int c1) {
              int max = Math.max(Math.max(a[a1], a[b1]), a[c1]);
              int min = Math.min(Math.min(a[a1], a[b1]), a[c1]);
              int median = max ^ min ^ a[a1] ^ a[b1] ^ a[c1];
              if (median == a[a1])
                  return a1;
              if (median == a[b1])
                  return b1;
              return c1;
          }
      
          // takes the last element as pivot
          private int partition(int low, int high) {
              int pivot = a[high];
              // Index of smaller element
              int i = (low - 1);
              for (int j = low; j <= high - 1; j++) {
                  if (a[j] <= pivot) {
                      i++;
                      swap(i, j);
                  }
              }
              swap(i + 1, high);
              return (i + 1);
          }
      
          // The main function that implements Introsort
          // low --> Starting index,
          // high --> Ending index,
          // depthLimit --> recursion level
          private void sortDataUtil(int begin, int end, int depthLimit) {
              if (end - begin > 16) {
                  if (depthLimit == 0) {
                      this.heapSort(begin, end);
                      return;
                  }
                  depthLimit = depthLimit - 1;
                  int pivot = findPivot(begin, begin + ((end - begin) / 2) + 1, end);
                  swap(pivot, end);
                  int p = partition(begin, end);
                  sortDataUtil(begin, p - 1, depthLimit);
                  sortDataUtil(p + 1, end, depthLimit);
              } else {
                  insertionSort(begin, end);
              }
          }
      
          private void sortData() {
              int depthLimit = (int) (2 * Math.floor(Math.log(n) / Math.log(2)));
              this.sortDataUtil(0, n - 1, depthLimit);
          }
      
          private void printData() {
              for (int i = 0; i < n; i++)
                  System.out.print(a[i] + " ");
          }
      
          public static void main(String args[]) throws IOException {
              int[] inp = {2, 10, 24, 2, 10, 11, 27, 4, 2, 4, 28, 16, 9, 8, 28, 10, 13, 24, 22, 28, 0, 13, 27, 13, 3, 23, 18, 22, 8, 8};
              int n = inp.length;
              IntroSort introsort = new IntroSort(n);
              for (int i = 0; i < n; i++) {
                  introsort.dataAppend(inp[i]);
              }
              introsort.sortData();
              introsort.printData();
          }
      }
      
    • 给 6000 个元素排序,堆排序、插入排序、快速排序、内省排序 效率对比(ms):

      效率对比

    • 缺点:和堆排序、快速排序一样,内省排序不稳定


  • 蒂姆排序(Timsort):
    • 插入排序 + 归并排序

    • 结合插入排序的直观和归并排序的效率,同时改进了归并排序中的顺序比较大小(少做无用功),在 Java 和 Android 操作系统内部使用广泛

    • 最坏情况下的时间复杂度是: O ( n l o g n ) O(nlogn) O(nlogn)

    • 可以看作是以块(run)为单位的归并排序(块内部元素已排好序)

    • 观察下面随机序列中相邻的数:
      大多相邻的数并非大小交替
      随机序列内部可能含有多个递增或者递减的子序列
      我们发现大多相邻的数并非大小交替,而是构成多个递增或递减子序列,蒂姆排序正是利用此特性减少操作

    • 大致流程

      1. 找出序列中各个递增和递减的子序列(如太短,二分查找,插入排序)
      2. 把这些子序列放入堆栈中(临时存储)
      3. 按照规则合并这些块(先合并最短,批处理归并,跳跃式预测

    • 成块插入模拟图:(跳跃式 + 批处理)
      蒂姆排序成块插入

    • 实现示例

      public class TimSort {
      static int MIN_MERGE = 32;
      
      public static int minRunLength(int n) {
          assert n >= 0;
          // Becomes 1 if any 1 bits are shifted off
          int r = 0;
          while (n >= MIN_MERGE) {
              r |= (n & 1);
              n >>= 1;
          }
          return n + r;
      }
      
      public static void insertionSort(int[] arr, int left, int right) {
          for (int i = left + 1; i <= right; i++) {
              int temp = arr[i];
              int j = i - 1;
              while (j >= left && arr[j] > temp) {
                  arr[j + 1] = arr[j];
                  j--;
              }
              arr[j + 1] = temp;
          }
      }
      
      // Merge function merges the sorted runs
      public static void merge(int[] arr, int l, int m, int r) {
          // Original array is broken in two parts left and right array
          int len1 = m - l + 1, len2 = r - m;
          int[] left = new int[len1];
          int[] right = new int[len2];
          for (int x = 0; x < len1; x++) {
              left[x] = arr[l + x];
          }
          for (int x = 0; x < len2; x++) {
              right[x] = arr[m + 1 + x];
          }
          int i = 0;
          int j = 0;
          int k = l;
      
          // After comparing, we merge those two array in larger sub array
          while (i < len1 && j < len2) {
              if (left[i] <= right[j]) {
                  arr[k] = left[i];
                  i++;
              } else {
                  arr[k] = right[j];
                  j++;
              }
              k++;
          }
      
          // Copy remaining elements of left, if any
          while (i < len1) {
              arr[k] = left[i];
              k++;
              i++;
          }
      
          // Copy remaining elements of right, if any
          while (j < len2) {
              arr[k] = right[j];
              k++;
              j++;
          }
      }
      
      public static void timSort(int[] arr, int n) {
          int minRun = minRunLength(MIN_MERGE);
          // Sort individual subarrays of size RUN
          for (int i = 0; i < n; i += minRun) {
              insertionSort(arr, i, Math.min((i + MIN_MERGE - 1), (n - 1)));
          }
          // Merge from size RUN (or 32). It will merge to form size 64, 128, 256 and so on
          for (int size = minRun; size < n; size = 2 * size) {
              // After every merge, we increase left by 2*size
              for (int left = 0; left < n;
                   left += 2 * size) {
                  // Find ending point of left sub array mid+1 is starting point of right sub array
                  int mid = left + size - 1;
                  int right = Math.min((left + 2 * size - 1), (n - 1));
                  // Merge sub array arr[left.....mid] & arr[mid+1....right]
                  if (mid < right)
                      merge(arr, left, mid, right);
              }
          }
      }
      
      public static void printArray(int[] arr, int n) {
          for (int i = 0; i < n; i++) {
              System.out.print(arr[i] + " ");
          }
          System.out.print("\n");
      }
      
      public static void main(String[] args) {
          int[] arr = {-2, 7, 15, -14, 0, 15, 0, 7, -7, -4, -13, 5, 8, -14, 12};
          int n = arr.length;
          System.out.println("Given Array is");
          printArray(arr, n);
          timSort(arr, n);
      
          System.out.println("After Sorting Array is");
          printArray(arr, n);
      	}
      }
      
    • 特点】:稳定便于多列列表排序应用广泛



【附录】为什么排序算法的复杂度不可能小于 O ( n l o g n ) O(nlogn) O(nlogn)

  • 对于任意一个序列,其元素有很多排列方式,其中:

    • 最小的序列是将其中每一个元素从小到大排好序

  • 假定有序列 a 1 , a 2 , . . . , a i , . . . , a j , . . . , a N a_{1},a_{2},...,\bm{a_{i}},...,\bm{a_{j}},...,a_{N} a1,a2,...,ai,...,aj,...,aN a 1 , a 2 , . . . , a j , . . . , a i , . . . , a N a_{1},a_{2},...,\bm{a_{j}},...,\bm{a_{i}},...,a_{N} a1,a2,...,aj,...,ai,...,aN,除了在第 i 个和第 j 个位置上的元素彼此互换(i < j),其他元素都相同

    • 如果 a i ≤ a j a_{i} ≤ a_{j} aiaj,第一个序列就小于第二个序列
    • 如果 a j ≤ a i a_{j} ≤ a_{i} ajai,则第二个序列小于第一个序列

  • 假如我们做 k 次比较,最多能区分出 2 k \bm{2^k} 2k 种不同序列的大小

    • 如果我们有 M 种序列,要区分出它们的大小,需要 logM 次比较
      区分出N!种不同序列的大小所需要的比较次数,不能少于包含N!个叶节点的二叉树的高度
  • 结论证明:(根据上面的推导)

    • N N N 个元素组成的数组能排列出 N ! N! N! 种序列
    • 因此,要选出其中最小的一个,至少需要 l o g N ! logN! logN! 次比较
    • 使用斯特林公式: l n N ! = N l n N − N + O ( l n N ) lnN!=NlnN−N+O(lnN) lnN!=NlnNN+O(lnN)
    • 可以得到复杂度: l o g N ! = O ( N l o g N ) logN!=O(NlogN) logN!=O(NlogN)
    • 综上,任何排序算法的复杂度都不可能小于 O ( n l o g n ) O(nlogn) O(nlogn)



【本节思考题】




总结

  • 通过阅读 1.4.3,我们了解了在一些特殊情况下(出于多维要求),是需要寻找一些更好的排序算法的(混合排序算法)。同时,如果能结合不同排序算法的特点去不断 “强化” 少做无用功原则,就能慢慢摸索到一些算法的敲门,从而找到一点感觉了。
    第一章思维导图

  • 至此,第一章学习结束。我们主要阅读学习了计算机软硬件的发展史、大O表示法、复杂度及数量级概念、逆向思维、少做无用功原则、递归和分治思想、直观和高效的排序算法等内容。最后,感谢吴军老师的开源授权,以及DW社区的积极组织。



参考资料

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值