algs4-1.4.11: 这次 GPT4 算错了时间复杂度
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 和你关键词之间的关联性, 然后给出它认为可能正确答案. 嗯…这个类比,其实不是非常恰当. 但是勉强可以辅助理解. 简单说就是: 虽然是同一个模型, 当我们输入不同的提示词组合, 模型内部处理问题的侧重点, 其实是不一样的.这就导致, 我们可能要适当变换提示词, 或者基于上下文进一步引导, 才能得到符合我们需要的答案.