1.4.11 为StaticSETofInts添加一个实列方法 howMany() (请见表1.2.15),找出给定键的出现次数且在最坏情况下所需的运行时间应该和logN成正比。

我的解题思路

  • 首先是找到 表1.2.15 (P61~P62), 把对应代码敲下来:
package algs4;

import java.util.Arrays;

import edu.princeton.cs.algs4.StdOut;

public class StaticSETofInts {
    private int[] a;

    public StaticSETofInts(int[] keys) {
        a = new int[keys.length];
        for (int i = 0; i < keys.length; i++) {
            a[i] = keys[i]; // 保护性复制.
        }
        Arrays.sort(a);
    }

    public boolean contains(int key) {
        return rank(key) != -1;
    }

    private int rank(int key) {
        // 二分查找.
        int lo = 0;
        int hi = a.length - 1;
        while (lo <= hi) {
            // 键要么存在于 a[lo..hi] 中, 要么不存在.
            int mid = lo + (hi - lo) / 2;
            if (key < a[mid])
                hi = mid - 1;
            else if (key > a[mid])
                lo = mid + 1;
            else
                return mid;
        }
        return -1;
    }
}
  • 设计一个简易够用的测试用例:
    public static void main(String[] args) {
        int key = 2;
        int[] a = { key, key };
        int[] b = { key, key, key };
        int[] c = { key, key, key, key };
        int[] d = { key, key, key, key, key };

        StaticSETofInts objA = new StaticSETofInts(a);
        StaticSETofInts objB = new StaticSETofInts(b);
        StaticSETofInts objC = new StaticSETofInts(c);
        StaticSETofInts objD = new StaticSETofInts(d);

        StdOut.println("key=" + key + "\t" + Arrays.toString(a) + "\t\t\thowMany = " + objA.howMany(key));
        StdOut.println("key=" + key + "\t" + Arrays.toString(b) + "\t\thowMany = " + objB.howMany(key));
        StdOut.println("key=" + key + "\t" + Arrays.toString(c) + "\t\thowMany = " + objC.howMany(key));
        StdOut.println("key=" + key + "\t" + Arrays.toString(d) + "\t\thowMany = " + objD.howMany(key));

        key += 1;
        StdOut.println("key=" + key + "\t" + Arrays.toString(a) + "\t\t\thowMany = " + objA.howMany(key));
        StdOut.println("key=" + key + "\t" + Arrays.toString(b) + "\t\thowMany = " + objB.howMany(key));
        StdOut.println("key=" + key + "\t" + Arrays.toString(c) + "\t\thowMany = " + objC.howMany(key));
        StdOut.println("key=" + key + "\t" + Arrays.toString(d) + "\t\thowMany = " + objD.howMany(key));
    }
  • 实现 howMany() 方法: 因为有1.4.10作为基础,所以思路也很容易想到. 我当时在检索 1.4.11 的解法时, 有些题解也提前泄露了 1.4.11 可能的思路.

    • 直接计算key的最大和最小索引, 然后相减即可.

    • 因为本质上是进行了常数次二分查找, 所以时间复杂度还是 O(logN).

      private int rankMin(int key) {
          // 二分查找.
          int lo = 0;
          int hi = a.length - 1;
          while (lo <= hi) {
              // 键要么存在于 a[lo..hi] 中, 要么不存在.
              int mid = lo + (hi - lo) / 2;
              if (key < a[mid])
                  hi = mid - 1;
              else if (key > a[mid])
                  lo = mid + 1;
              else if (mid - 1 >= 0 && a[mid - 1] == key)
                  hi = mid - 1;
              else
                  return mid;
          }
          return -1;
      }
    
      private int rankMax(int key) {
          // 二分查找.
          int lo = 0;
          int hi = a.length - 1;
          while (lo <= hi) {
              // 键要么存在于 a[lo..hi] 中, 要么不存在.
              int mid = lo + (hi - lo) / 2;
              if (key < a[mid])
                  hi = mid - 1;
              else if (key > a[mid])
                  lo = mid + 1;
              else if (mid + 1 < a.length && a[mid + 1] == key)
                  lo = mid + 1;
              else
                  return mid;
          }
          return -1;
      }
    
      // for 1.4.11
      public int howMany(int key) {
          int min = rankMin(key);
          if (-1 == min)
              return 0;
          return 1 + rankMax(key) - rankMin(key);
      }
    
    • 用例代码输出如下:
      key=2   [2, 2]                  howMany = 2
      key=2   [2, 2, 2]               howMany = 3
      key=2   [2, 2, 2, 2]            howMany = 4
      key=2   [2, 2, 2, 2, 2]         howMany = 5
      key=3   [2, 2]                  howMany = 0
      key=3   [2, 2, 2]               howMany = 0
      key=3   [2, 2, 2, 2]            howMany = 0
      key=3   [2, 2, 2, 2, 2]         howMany = 0
    

看看 GPT4 有何高见!

  • 首先, 它用自己的话重复了以下我的问题:

    在已有的 StaticSETofInts 类中添加一个新的实例方法 howMany() 可以使用二分查找的原理来实现,具体可以通过找到数组中最左边和最右边与目标数值相等的元素,然后计算这两个元素之间的数量,其中最坏情况的运行时间和 logN 成正比。

所以, 我可以认为它是已经 “理解” 了我的问题的.

  • 但是它给出的答案, 明显是不符合题意的.它的答案, 最坏情况下, 明显是 O(N) 的. 因为它打算往左右遍历所有的和key相同的元素, 然后基于遍历来计数:
    public int howMany(int key) {
        // 如果数组不含该元素,直接返回0
        if (!contains(key)) {
            return 0;
        }

        int i = rank(key);
        int count = 1;

        // 向左查找
        for (int j = i-1; j >= 0 && a[j] == key; j--) {
            count++;
        }

        // 向右查找
        for (int j = i+1; j < a.length && a[j] == key; j++) {
            count++;
        }

        return count;
    }
  • 当然, 在我最终让它重新检查下算法的时间复杂度后, 给出了符合要求的答案. 虽然和我的答案不完全一样. 但是本质上是没有差别的.

小感: 机器学习时代, 人人都应该学习点机器学习的 基本知识.

如果对机器学习完全不了解的话, 可能会很困惑: 它明明已经 “理解” 了我的问题, 它甚至还重复了一遍关键点.

基于我有限的了解, 机器学习模型, 有好多 weight 值, 有点类似于网页的 Page Rank 排名. 当你在搜索引擎中,输入不同的关键词组合, 得到的组合不一样. 是因为 搜索引擎 综合考虑了网页的 PR 和你关键词之间的关联性, 然后给出它认为可能正确答案. 嗯…这个类比,其实不是非常恰当. 但是勉强可以辅助理解. 简单说就是: 虽然是同一个模型, 当我们输入不同的提示词组合, 模型内部处理问题的侧重点, 其实是不一样的.这就导致, 我们可能要适当变换提示词, 或者基于上下文进一步引导, 才能得到符合我们需要的答案.

附图: GPT4 完整聊天记录.

chatgpt4

参考: