CS61B Project 1A
利用双向链表和循环数组的数据结构构造
LinkedListDeque
sentinel node无疑是重点,联系 DLList 内容,构建双向链表,其中 sentinel node 的环形链表是关键
学习查找中 sentinel node中,查找的一些有益资料:
How does a sentinel node offer benefits over NULL? 说明了哨兵节点关于null的优势
Using sentinel nodes in Linked List operations 有关在Java中使用哨兵节点的 RemoveLinkedList 操作
哨兵节点:思想简单,效果很棒的的编程算法 - IOT物联网小镇的文章 - 知乎
关于 addFirst, addLast, removeFirst, removeLast 方法,核心在于理清单哨兵节点的环形列表逻辑,理清这个逻辑之后,实现就不算特别难,要理清楚知识点还是得在实践题目里练习啊,我最开始半懵办懂地入手被 sentinel node折磨红温,但真正理清楚了其实也就那样。
size:实现 size 需要花费恒定的时间,所以其时间复杂度为 O(1)。
DeepCopy:深层复制 意味着不仅复制对象本身,还要复制对象内部所有引用的对象。在双向链表实现中,这意味着
要创建新的节点,而不是仅复制节点的引用。这样,当修改原始队列 other 时,新的队列不会受到影响。
在 Josh 的 视频引导 中提供了一种循环调用 addLast((T) other.get(i)); 的方法,将 other 队列中的每个元素添加到新队列中。
Josh 也在视频中说到这是一种比较容易但有点低效的方法,因为get(int index) 方法的时间复杂度为 O(n),在复制构造函数中,通过循环调用 get(i),整体时间复杂度为 O(n²),这样就使得效率更为低效。
ArrayDeque
(折磨啊,尤其是调整数组大小那一块,确实不容易,相当棘手 )
Circular ArrayDeque:
将数组的末尾和开头连接起来,形成一个环状结构,使得队列在数组的两端均可插入和删除,避免了浪费空间的情况。
Implementation of Deque using circular array
resizing array
正确调整数组大小是解决数组队列的难点与重点,非常棘手。需要正确地调整数组大小(即 扩容 和 缩容),确保程序在任何给定时间使用的内存量与元素数量成正比。
使用系数(Usage Factor) :
定义:使用系数 = size / length,即元素数量与数组容量的比值。
要求:
-
当数组长度 ≥ 16 时,使用系数应始终至少为 25% 。
-
对于长度 < 16 的数组,使用系数可以任意低。
扩容——当数组已满,即 size == length 时,需要进行扩容。
扩容倍增策略:将数组容量扩大为原来的 2 倍。
缩容——当使用系数 size / length < 0.25,且 length >= 16 时,需要进行缩容。
缩容减半策略:将数组容量缩小为原来的 1/2。
最小容量限制:为了避免数组容量无限制地缩小,通常设定一个最小容量(例如 8)。当容量小于最小容量时,不进行缩容。
当数组需要 扩容 或 缩容 时,需要将元素从旧数组复制到新数组中。由于数组是环形的,元素在数组中的顺序可能不是连续的,因此在复制元素时,需要正确地处理索引,以确保元素按队列的顺序排列。
由于数组是环形的,front + i 可能超过数组的末尾,需要回绕到数组的开头。模运算(% length)的作用是确保索引始终在数组的有效范围内(0 到 length - 1),从而正确地访问元素。取模运算很巧妙,也很关键。
从 i = 0 到 i = size - 1,对于每个 i,计算旧数组中的索引 oldIndex = (front + i) % length,然后将 items[oldIndex] 复制到 newItems[i]。
for (int i = 0; i < size; i++) {
newItems[i] = items[(front + i) % length]
}
下面是对此模运算的具体分析——
length(旧数组容量)为 8; front 为 6; size 为 5:
这意味着队列中有 5 个元素,front 指向索引 6。
旧数组状态:
索引: 0 1 2 3 4 5 6 7
元素: [C] [D] [E] [ ] [ ] [ ] [A] [B]
^
front
我们需要按照队列的顺序,将元素复制到新数组中,使它们在新数组中从索引 0 开始连续排列。
例如:i = 0
- 旧数组索引:
(front + 0) % length = (6 + 0) % 8 = 6 newItems[0] = items[6] = A
i = 2
- 旧数组索引:
(front + 2) % length = (6 + 2) % 8 = 0(注意这里超过了数组末尾,回到了索引0) newItems[2] = items[0] = C
新数组状态则变更为:
索引: 0 1 2 3 4 5 6 7 ...
元素: [A] [B] [C] [D] [E] [ ] [ ] [ ] ...
在完成元素复制后,指针需要重置,front 重置为 0,rear 重置为 size(即 5)。
在进行添加操作addFirst 和 addLast 方法前,检查是否需要扩容(扩容操作需要在更新 front 或 rear 指针之前进行)
if (size == length) {
resize(length * 2);
}
在进行删除操作removeFirst 和 removeLast 方法前,检查是否需要缩容(缩容操作需要在更新 front 或 rear 指针和 size 之后进行)
if (length >= 16 && size < length / 4) {
resize(length / 2);
}
removeLast 需要先更新 rear 指针再进行后续操作,因为rear 指针指向下一个可插入的位置,而不是当前最后一个元素的位置,因此,要移除最后一个元素,需要先将 rear 指针向前移动一个位置,指向最后一个实际存在的元素。
resizie 方法的两大重点:
环绕逻辑:使用模运算 (front + i) % length 确保即使队列元素在原数组中是环绕排列的,也能正确地复制到新数组中。
指针重置:复制完成后,将 front 重置为 0,rear 设置为 size,以确保新数组中的指针位置一致且直观。
最后,写了一点GTP助教 prompt 帮助学习,O1-Preview的助教表现还是相当不错,但是有使用限额,所以也用了 O1-mini,作为自学者,没有 UCB 这种顶级大学的助教资源,但好在互联网和 AI 的发展让普通人学习知识的门槛进一步降低了。
GPT 助教 Prompt
写了一段 prompt 来利用 GPT 扮演 CS61B 中的助教角色——
我现在将要进行 CS61B 课程中一个project,关于 双向链表 的数据结构项目练习,我需要你作为我的助教角色,来帮助我解答我在做此项目中的疑问与困惑,作为助教,以下几点要求请你铭记并执行:
- 由我独自编写:构建项目中,我提交的所有项目代码(骨架代码除外)应由我单独编写,解决微小子问题的小片段除外。
- 禁止为我直接提供完善的代码答案:这是第一条规则的延伸,因为学术不诚实的黄金法则是,你不应该声称对不属于你的工作负责。所以你必须作为助教帮助我独立完成构建此项目,而不是作为工具为我提供违反我独立完成的作弊代码!
- 引用来源:当从其他地方(网站/大模型/他人GitHUb仓库)那里获得有关代码作业的重要帮助时,应该在源代码中的某个位置在注释中使用
@source标签引用该帮助。 引用示例如下——
// @source This code was generated by ChatGPT. It reads and parses
// all integers from the file, which I then pass into my computeSum method.
...
4. 允许执行:讨论解决问题的方法;针对问题解决方案的重要概念性想法;讨论代码中的特定语法问题和错误;使用在网上找到的小代码片段来解决小问题(例如,谷歌搜索“大写字符串java”可能会引导找到一些示例代码,可以将其复制并粘贴到解决方案中),此类用法应在项目代码中作为注释引用!
- CheckStyle: coding-style非常重要,在你担任我的助教途中,请时刻提醒检查完善我的 coding-style。