链表(14) --hot 100 in leetcode
相交链表 算法概述 原题
题目要求为找到两个链表中相交的元素(存储位置需要相对链表末尾相同,也就是从后往前数有几个节点)。我的想法是尽可能节约时间复杂度,我一开始想的是遍历到末尾再回头遍历,其实和这个方案得到的时间复杂度是一样的,但是我以为还有其他办法。但实际上是必须要这么多次遍历才够的。也就是要用双指针进行同时遍历,不过不是到末尾返回,而是 到对方的链表中继续遍历 。
时间复杂度为O(n+m):双指针同时遍历,最多遍历完(返回null情况)
空间复杂度为O(1):双指针存储
JAVA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Solution { public ListNode getIntersectionNode (ListNode headA, ListNode headB) { if (headA == null || headB == null ) { return null ; } ListNode pA = headA, pB = headB; while (pA != pB) { pA = pA == null ? headB : pA.next; pB = pB == null ? headA : pB.next; } return pA; } }
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution {public : ListNode *getIntersectionNode (ListNode *headA, ListNode *headB) { if (headA == nullptr || headB == nullptr ) { return nullptr ; } ListNode *pA = headA, *pB = headB; while (pA != pB) { pA = pA == nullptr ? headB : pA->next; pB = pB == nullptr ? headA : pB->next; } return pA; } };
注意 pA = pA == nullptr ? headB : pA->next;
:可以使用这样的语句在单句代码中完成判断和赋值更新(先判断=
右边,再到=
) 这道题对 寻找交点 这种任务提供了一种新的思路,即从整体上,双指针的遍历次数已经确定的情况下, 如何安排遍历 是重点,也就是怎样能够使双指针 在不同的起点,抵达同步的终点 。
反转链表 算法概述 原题
本题目要求反转链表。链表的结构是拥有灵活的前驱和后驱,非常便于插入,所以应该以重新插入为重点,把每个节点想象成三个节点(前驱-当前节点-后驱)的联合结构。
时间复杂度为O(n):遍历链表
空间复杂度为O(1):三个变量(存在于不同作用域)存储这个三元关系
JAVA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public ListNode reverseList (ListNode head) { ListNode prev = null ; ListNode curr = head; while (curr != null ) { ListNode next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; } }
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution {public : ListNode* reverseList (ListNode* head) { ListNode* prev = nullptr ; ListNode* curr = head; while (curr) { ListNode* next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; } };
注意 链表的特性便在于灵活的插入与复杂的遍历,所以必须要在遍历的同时利用灵活的插入改变前驱和后驱来调整链表的顺序,不仅对于链表的顺序,在面对任何操作链表的问题时,应该记住它的灵活性所在,并加以利用。
代码中难以理解的就是,虽然可以用空节点填补头结点的前驱空缺,但实际上链表只包含了自己的信息和下一个节点的信息,所以 prev 在这里的作用只是一个为了循环遍历服务的中间变量,就看的很奇怪,实际上只有 next 和 curr 是实际操作的主体,而且 next 因为可以通过当前节点访问,所以每次循环都会重新生成,是临时的。
回文链表 算法概述 原题
本题要求为判断链表是否为互文的。互文的关键特征是链表是否是对称的,也就是说要找到中间节点,再 翻转后半部分 ,再进行比较才行。
时间复杂度为O(n):只需遍历找到中间节点
空间复杂度为O(1):只修改原链表
JAVA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class Solution { public boolean isPalindrome (ListNode head) { if (head==null ){ return true ; } ListNode firstHalfEnd=endOfFirstHalf(head); ListNode secondHalfStart=reverseList(firstHalfEnd.next); ListNode p1=head; ListNode p2=secondHalfStart; boolean result=true ; while (result&&p2!=null ){ if (p1.val!=p2.val){ result=false ; } p1=p1.next; p2=p2.next; } return result; } private ListNode reverseList (ListNode head) { ListNode prev=null ; ListNode curr=head; while (curr!=null ){ ListNode nextTemp=curr.next; curr.next=prev; prev=curr; curr=nextTemp; } return prev; } private ListNode endOfFirstHalf (ListNode head) { ListNode fast=head; ListNode slow=head; while (fast.next!=null &&fast.next.next!=null ){ fast=fast.next.next; slow=slow.next; } return slow; } }
C++
注意 学会用fast
和slow
快慢指针找节点(之前也有双指针题目用过快慢指针)。 牢记 反转链表 的思路,可以用作其他题的模块。
环形链表 算法概述 原题
本题要求判断链表中是否有环。可以用 快慢指针 解决。
时间复杂度为O(n):就按照慢指针最多移动多远算
空间复杂度为O(1):快慢指针
JAVA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Solution { public boolean hasCycle (ListNode head) { if (head == null || head.next == null ) { return false ; } ListNode slow = head; ListNode fast = head.next; while (slow != fast) { if (fast == null || fast.next == null ) { return false ; } slow = slow.next; fast = fast.next.next; } return true ; } }
C++
注意 像前面的求交点问题一样,都是不可避免的需要一个几乎完整的循环遍历,但怎么让其中的双指针通过 相遇 把问题解决,是这类题目的重点。可以为双指针赋予不同的起点、速度等等,来求得不同的目标。
环形链表 II 算法概述 原题
本题要求为返回进入环的那个节点(前一道题只需要判断是否有环)。我想的其实是反转链表,把相遇点之前的链表反转,然后反向遍历,但是还可以通过计算直接得出一个更加方便的方法,也就是在快慢指针相遇的时候, 再从链表头释放一个指针 ,这个指针会和不停绕着环转的慢指针相遇在入环点🤨。
时间复杂度为O(n):全部放在一个while的作用就在这里
空间复杂度为O(1):三指针
JAVA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class Solution { public ListNode detectCycle (ListNode head) { if (head == null ) { return null ; } ListNode slow = head, fast = head; while (fast != null ) { slow = slow.next; if (fast.next != null ) { fast = fast.next.next; } else { return null ; } if (fast == slow) { ListNode ptr = head; while (ptr != slow) { ptr = ptr.next; slow = slow.next; } return ptr; } } return null ; } }
C++
注意 其中 a 是环外距离, b 快慢指针走过的环内距离, c 是相遇点顺时针计算离入环点的距离。 可见有时不用很复杂的代码解决问题,和之前的矩阵顺时针旋转类似,用纸笔也可以计算出通用的公式,更方便执行。
合并两个有序链表 算法概述 原题
题目要求为将给出的有序列表按照升序合并。还是双指针,同时因为链表很灵活,对于输入链表可以在遍历时同时修改,所以不需要创建新的变量。很像矩阵的查找目标值的思路( Z字形移动 ),因为实际的代码逻辑就是找单次比较中较小的,然后移动相应指针。
时间复杂度为O(n+m):都需要遍历比较
空间复杂度为O(1):只需要一个变量标记目前较大值
JAVA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution { public ListNode mergeTwoLists (ListNode list1, ListNode list2) { ListNode prehead=new ListNode (-1 ); ListNode prev=prehead; while (l1!=null &&l2!=null ){ if (l1.val<=l2.val){ prev.next=l1; l1=l1.next; }else { prev.next=l2; l2=l2.next; } prev=prev.next; } prev.next=l1==null ?l2:l1; return prehead.next; } }
C++
注意 可以用链表的头结点直接操作,但是需要记得 标记起始点 。 因为双指针的操作需要沿着它们各自链表,所以需要一个额外的指针将比较的结果串起来, 它落后于链表遍历指针 。
两数相加 算法概述 原题
本题要求为求出两个以逆序且各个节点用以表示位数的链表表示的数字之和。就是直接相加,同时保留进位。
时间复杂度为O(m+n):都遍历一遍就行
空间复杂度为O(1):返回的链表不算
JAVA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Solution { public ListNode addTwoNumbers(ListNode l1, ListNode l2) { // 创建新的链表返回 ListNode head = null, tail = null; // 进位 int carry = 0; while (l1 != null || l2 != null) { // next有则移动,无则为0(长度不相同) int n1 = l1 != null ? l1.val : 0; int n2 = l2 != null ? l2.val : 0; int sum = n1 + n2 + carry; if (head == null) { // 题目要求单节点只保存一位 head = tail = new ListNode(sum % 10); } else { // 头结点用于返回,尾节点用于更新返回链表 tail.next = new ListNode(sum % 10); tail = tail.next; } // 进位计算 carry = sum / 10; // 移动指针 if (l1 != null) { l1 = l1.next; } if (l2 != null) { l2 = l2.next; } } // 如果最后还有进位,就创建新节点 if (carry > 0) { tail.next = new ListNode(carry); } return head ; } }
C++
注意 虽然思路不复杂,但是搭建一个优雅的代码框架还是很困难的。 要注意用int n1 = l1 != null ? l1.val : 0;
将值与遍历分离(因为每一个节点只能存一个数字,但和会是两位的)。 要常温常新。
删除链表的倒数第N个节点 算法概述 原题
本题要求为删除链表中倒数的第n个节点并返回链表。 快慢指针 ,快指针到头了,恰好慢指针也到该删除的节点前面。
时间复杂度为O(n):一个循环
空间复杂度为O(1):双指针
JAVA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { public ListNode removeNthFromEnd (ListNode head, int n) { ListNode dummy = new ListNode (0 , head); ListNode first = head; ListNode second = dummy; for (int i = 0 ; i < n; ++i) { first = first.next; } while (first != null) { first = first.next; second = second.next; } second.next = second.next.next; ListNode ans = dummy.next; return ans; } }
C++
注意 ListNode dummy = new ListNode(0, head);
: dummy 是为了处理边界情况,也就是删除的就是头结点。second.next = second.next.next;
:我前面想删除节点得找到前后节点,但实际上只需要找到前一个节点即可,这就是数据结构不熟。 思路很简单,但是怎样优雅的实现需要学习与思考。
两两交换链表中的节点 算法概述 原题
本题要求将链表中的两两相邻节点原地交换位置。就是正常的遍历交换(迭代)。
时间复杂度为O(n):节点要遍历完
空间复杂度为O(1);和上题一样需要哑结点和一些其他的中间变量
JAVA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public ListNode swapPairs (ListNode head) { ListNode dummyHead = new ListNode (0 ,head); ListNode temp = dummyHead; while (temp.next != null && temp.next.next != null) { ListNode node1 = temp.next; ListNode node2 = temp.next.next; temp.next = node2; node1. next = node2. next; node2. next = node1; temp = node1; } return dummyHead.next; } }
C++
注意 ListNode dummyHead = new ListNode(0,head);
:链表原地操作往往需要一个哑结点辅助处理边界。while (temp.next != null && temp.next.next != null)
: temp 只参与判断是否需要交换,交换本身 node1 和 node2 就能完成。 思路不难,重在实现与全面的考虑。
K个一组翻转链表 算法概述 原题
本题要求为以k为一组翻转子链表。其实思路不复杂,关键是怎么组织需要使用到的中间变量。
时间复杂度为O(n):一次遍历
空间复杂度为O(1);依托中间变量
JAVA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class Solution { public ListNode reverseKGroup (ListNode head, int k) { ListNode dummy = new ListNode (0 ,head); ListNode pre = dummy; while (head != null ) { ListNode tail = pre; for (int i = 0 ; i < k; ++i) { tail = tail.next; if (tail == null ) { return dummy.next; } } ListNode nex = tail.next; ListNode[] reverse = myReverse(head, tail); head = reverse[0 ]; tail = reverse[1 ]; pre.next = head; tail.next = nex; pre = tail; head = tail.next; } return dummy.next; } public ListNode[] myReverse(ListNode head, ListNode tail) { ListNode prev = tail.next; ListNode curr = head; while (prev != tail) { ListNode next = curr.next; curr.next = prev; prev = curr; curr = next; } return new ListNode []{tail, head}; } }
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Solution {public : ListNode* reverseKGroup (ListNode* head, int k) { ListNode* dummy=new ListNode (0 ,head); ListNode* pre=dummy; while (head!=nullptr ){ ListNode* tail=pre; for (int i=0 ;i!=k;++i){ tail=tail->next; if (tail==nullptr ){ return dummy->next; } } ListNode* nex=tail->next; tie (head,tail)=myReverse (head,tail); pre->next=head; tail->next=nex; pre=tail; head=tail->next; } return dummy->next; } private : pair<ListNode*,ListNode*> myReverse (ListNode* head, ListNode* tail) { ListNode* prev=tail->next; ListNode* curr=head; while (prev!=tail){ ListNode* next=curr->next; curr->next=prev; prev=curr; curr=next; } return {tail,head}; } };
重要实例方法及属性(C++) pair<ListNode*,ListNode*>
:使用pair容器类返回多个值tie(head,tail)=myReverse(head,tail)
:std::tie不仅可以解包std::tuple,也可以解包std::pair
注意 ListNode* dummy=new ListNode(0,head);
这种需要操作子单位的题目都需要哑结点。 代码中用到pre
来存储子链表的前一个节点,这里需要考虑到的是链表本身的特性,即 必须要有前一个节点,才能插入 。 以及用到了tail
来进行遍历,判断是否剩余节点满足翻转条件。这里需要注意的是,tail
不仅仅可用于遍历,还可用于 更新 pre
,这是很重要的。 以及还需要在翻转前保存子链表的下一个节点nex
,在翻转后重新连接。
pre
用于指向子链表前一个节点。
tail
用于指向子链表的最后一个节点,以及下一组子链表的pre
。
nex
指向子链表的后一个节点。
还需要注意的是在翻转中:while(prev!=tail)
是以tail
为终止条件,而不是while(curr!=null)
,需要注意翻转对象的性质改变辅助函数。
随机链表的复制 算法概述 原题
本题要求为深拷贝给出的链表(每个子节点包括后驱和一个随机索引)。难点在于顺序,或者说思路本身并不难,如何组织出一个行之有效的方法才难。 给出的解法是 三个循环:新节点(包含next)->random->新表 。核心思路就是在原链表上操作,这样会极大减少查询原链表信息的难度,会使所有操作简化为使用几次迭代的问题。
时间复杂度为O(n):遍历三次
空间复杂度为O(1):中间变量,输出不算
JAVA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution { public Node copyRandomList (Node head) { if (head == null ) { return null ; } for (Node node = head; node != null ; node = node.next.next) { Node nodeNew = new Node (node.val); nodeNew.next = node.next; node.next = nodeNew; } for (Node node = head; node != null ; node = node.next.next) { Node nodeNew = node.next; nodeNew.random = (node.random != null ) ? node.random.next : null ; } Node headNew = head.next; for (Node node = head; node != null ; node = node.next) { Node nodeNew = node.next; node.next = node.next.next; nodeNew.next = (nodeNew.next != null ) ? nodeNew.next.next : null ; } return headNew; } }
C++
注意 第一个循环中把新节点插入节点之间的最大原因,就是可以在第三个循环中的nodeNew.next = (nodeNew.next != null) ? nodeNew.next.next : null;
直接修改新节点的next
,而且只需要两次迭代即可。 同时也方便了第二个循环中的nodeNew.random = (node.random != null) ? node.random.next : null;
用以更新random,这样只需对当前节点的random进行迭代即可。 总而言之,核心的核心就是 把新节点插入到原链表中 ,这也属于基于原链表使操作更便捷,非常经典,需要牢记和熟练。
还有回溯+哈希表的解法,时间复杂度一样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution {public : unordered_map<Node*, Node*> cachedNode; Node* copyRandomList(Node* head) { if (head == nullptr) { return nullptr; } if (!cachedNode.count(head)) { Node* headNew = new Node (head->val); cachedNode[head] = headNew; headNew->next = copyRandomList(head->next); headNew->random = copyRandomList(head->random); } return cachedNode[head]; } };
排序链表 算法概述 原题
本题要求为升序排列给出的链表。使用 自底向上的归并排序 。 自底向上的归并排序,就是把原链表以1、2、4、6、8……等sublength的长度分为多个子链表,每次分成两个子链表后,将这两个子链表以“合并两个升序链表”的方式合并,因为是从1开始的,所以这样做没有问题。不断循环这样向上合并的过程,后面即使两个子链表剩的长度不够也没有问题,“合并两个升序链表”给出了解释。
时间复杂度为O(nlogn):归并排序
空间复杂度为O(1):原地操作
JAVA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Solution { public ListNode sortList (ListNode head) { if (head == null ) { return head; } int length = 0 ; ListNode node = head; while (node != null ) { length++; node = node.next; } ListNode dummyHead = new ListNode (0 , head); for (int subLength = 1 ; subLength < length; subLength <<= 1 ) { ListNode prev = dummyHead, curr = dummyHead.next; while (curr != null ) { ListNode head1 = curr; for (int i = 1 ; i < subLength && curr.next != null ; i++) { curr = curr.next; } ListNode head2 = curr.next; curr.next = null ; curr = head2; for (int i = 1 ; i < subLength && curr != null && curr.next != null ; i++) { curr = curr.next; } ListNode next = null ; if (curr != null ) { next = curr.next; curr.next = null ; } ListNode merged = merge(head1, head2); prev.next = merged; while (prev.next != null ) { prev = prev.next; } curr = next; } } return dummyHead.next; } public ListNode merge (ListNode head1, ListNode head2) { ListNode dummyHead = new ListNode (0 ); ListNode temp = dummyHead, temp1 = head1, temp2 = head2; while (temp1 != null && temp2 != null ) { if (temp1.val <= temp2.val) { temp.next = temp1; temp1 = temp1.next; } else { temp.next = temp2; temp2 = temp2.next; } temp = temp.next; } if (temp1 != null ) { temp.next = temp1; } else if (temp2 != null ) { temp.next = temp2; } return dummyHead.next; } }
C++
注意 自顶向下和自底向上的归并排序 时间复杂度一样 ,但是 空间复杂度上后者更优 。但自顶向上的应用更广泛,自底向上 适合链表 。
class Solution
中的dummyHead
用于处理整个链表的极端边界情况。
merge()
中的dummyHead
用于处理“合并两个升序链表”中的极端边界情况。
head1
是当前子链表对第一个子链表的头,head2
是当前子链表对第二个子链表的头。
next
指向下一个子链表对的头结点,curr
依据它来更新。
prev
用于存储前一个子链表对的最后一个节点,然后将合并完(分割过的)子链表对重新连接回来。
归并排序对于链表极为重要, 务必背记 。
合并k个升序链表 算法概述 原题
本题要求为合并给出链表中的所有升序子链表。采用的还是和上题一样的 分治法 。但是这里因为用的是递归,所以隐藏代码写的更深,没有写出来。用的是独立的两个递归函数共同工作。
时间复杂度为O(kn×logk):多组分治遍历的次数求和
空间复杂度为O(logk):栈
JAVA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class Solution { public ListNode mergeKLists (ListNode[] lists) { return merge(lists, 0 , lists.length - 1 ); } public ListNode merge (ListNode[] lists, int l, int r) { if (l == r) { return lists[l]; } if (l > r) { return null ; } int mid = (l + r) >> 1 ; return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1 , r)); } public ListNode mergeTwoLists (ListNode a, ListNode b) { if (a == null || b == null ) { return a != null ? a : b; } ListNode dummy = new ListNode (0 ); ListNode tail = dummy, aPtr = a, bPtr = b; while (aPtr != null && bPtr != null ) { if (aPtr.val < bPtr.val) { tail.next = aPtr; aPtr = aPtr.next; } else { tail.next = bPtr; bPtr = bPtr.next; } tail = tail.next; } tail.next = (aPtr != null ? aPtr : bPtr); return dummy.next; } }
C++
注意 两道题都用到了基本的排序,说明还是很重要的,“合并两个升序链表”还是要牢记、 把功能分离到两个递归函数里来工作,是比较复杂的,虽然说结构很清晰简单,但是栈内的顺序还是要理清楚,要多练习。
LRU缓存 算法概述 原题
本题要求设计一个满足getter和setter(get,put,capacity初始化)的类,LRU意为最近访问的数据在实例内优先级更高,且要求get()和put()在O(1)时间内完成。核心是在确保访问操作的时间复杂度的同时,根据capacity动态调整存储的键值对。使用 双向链表 实现。
JAVA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 class LRUCache { class DLinkedNode { int key; int value; DLinkedNode prev; DLinkedNode next; public DLinkedNode () {} public DLinkedNode (int _key, int _value) {key = _key;value = _value;} } private Map<Integer, DLinkedNode> cache = new HashMap <Integer, DLinkedNode>(); private int size; private int capacity; private DLinkedNode head, tail; public LRUCache (int capacity) { this .size = 0 ; this .capacity = capacity; head = new DLinkedNode (); tail = new DLinkedNode (); head.next = tail; tail.prev = head; } public int get (int key) { DLinkedNode node = cache.get(key); if (node == null ) { return -1 ; } moveToHead(node); return node.value; } public void put (int key, int value) { DLinkedNode node = cache.get(key); if (node == null ) { DLinkedNode newNode = new DLinkedNode (key, value); cache.put(key, newNode); addToHead(newNode); ++size; if (size > capacity) { DLinkedNode tail = removeTail(); cache.remove(tail.key); --size; } } else { node.value = value; moveToHead(node); } } private void addToHead (DLinkedNode node) { node.prev = head; node.next = head.next; head.next.prev = node; head.next = node; } private void removeNode (DLinkedNode node) { node.prev.next = node.next; node.next.prev = node.prev; } private void moveToHead (DLinkedNode node) { removeNode(node); addToHead(node); } private DLinkedNode removeTail () { DLinkedNode res = tail.prev; removeNode(res); return res; } }
C++
注意 标准的将自定义数据结构和常用容器复合的类定义,要注意体现数据结构的特征(e.g., moveToHead
直接删除和插入)。 在这个类中,哈希表和双向链表都存了一次数据,因为 哈希表是用于快速访问 ,而 双向链表是用于更新顺序 ,这里需要兼取两者的优势。 两个数据结构之间的交互或者数据结构本身的欠缺需要设置私有变量来弥补(e.g., size
和capacity
)。
常温常新