链表
链表
链表通过指针将一组零散的内存块串联在一起。其中,我们把内存块称为链表的“结点”。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。如图所示,我们把这个记录下个结点地址的指针叫作后继指针 next。
链表分类
链表和数组对比
1:存储空间
数组需要一块连续的内存空间来存储,对内存的要求比较高。如果我们申请一个 100MB 大小的数组,当内存中没有连续的、足够大的存储空间时,即便内存的剩余总可用空间大于 100MB,仍然会申请失败。
而链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用,所以如果我们申请的是 100MB 大小的链表,根本不会有问题。
2:CPU缓存
数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。
而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读
3:扩容
数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。
链表本身没有大小的限制,天然地支持动态扩容,这也是它与数组最大的区别。
4:操作性能
链表使用
1、理解指针或引用的含义
将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
在编写链表代码的时候,我们经常会有这样的代码:p->next=q。这行代码是说,p 结点中的next 指针存储了 q 结点的内存地址。
2、警惕指针丢失和内存泄漏
我们希望在结点 a 和相邻的结点 b 之间插入结点 x,假设当前指针 p 指向结点 a。如果我们将代码实现变成下面这个样子,就会发生指针丢失和内存泄露。
此时,p->next 指针在完成第一步操作之后,已经不再指向结点 b 了,而是指向结点 x。第 2 行代码相当于将 x 赋值给 x->next,自己指向自己。因此,整个链表也就断成了两半,从结点 b 往后的所有结点都无法访问到了。
所以,我们插入结点时,一定要注意操作的顺序,要先将结点 x的 next 指针指向结点 b,再把结点 a 的 next 指针指向结点 x,这样才不会丢失指针,导致内存泄漏。所以,对于刚刚的插入代码,我们只需要把第 1 行和第 2 行代码的顺序颠倒一下就可以了。
3、利用哨兵简化实现难度
如果我们在结点 p 后面插入一个新的结点,只需要下面两行代码就可以搞定。
如果要删除结点 p 的后继结点,我们只需要一行代码就可以搞定。
但是,当我们要向一个空链表中插入第一个结点,或者要删除链表中的最后一个结点,前面的删除代码就不 work 了,需要进行下面的特殊处理:
针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。这样代码实现起来就会很繁琐,不简洁,而且也容易因为考虑不全而出错。这个时候就需要借助哨兵来简化操作。
如果我们引入哨兵结点,在任何时候,不管链表是不是空,head 指针都会一直指向这个哨兵结点。我们也把这种有哨兵结点的链表叫带头链表。相反,没有哨兵结点的链表就叫作不带头链表。
4、重点留意边界条件处理
经常用来检查链表代码是否正确的边界条件有这样几个:
- 如果链表为空时,代码是否能正常工作?
- 如果链表只包含一个结点时,代码是否能正常工作?
- 如果链表只包含两个结点时,代码是否能正常工作?
- 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
相关题目
203、移除链表元素
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
输入:head = [], val = 1
输出:[]
输入:head = [7,7,7,7], val = 7
输出:[]
var removeElements = function(head, val) { const ret = new ListNode(0, head); let cur = ret; while(cur.next) { if(cur.next.val === val) { cur.next = cur.next.next; continue; } cur = cur.next; } return ret.next; };
206、反转链表
给你单链表的头节点
head
,请你反转链表,并返回反转后的链表。
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
输入:head = [1,2]
输出:[2,1]
var reverseList = function(head) { //双指针 if(!head || !head.next) return head; let temp = null, pre = null, cur = head; while(cur) { temp = cur.next; cur.next = pre; pre = cur; cur = temp; } return pre; };
24、两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
输入:head = [1,2,3,4]
输出:[2,1,4,3]
输入:head = [1]
输出:[1]
输入:head = []
输出:[]
var swapPairs = function (head) { let ret = new ListNode(0, head), temp = ret; while (temp.next && temp.next.next) { let cur = temp.next.next, pre = temp.next; pre.next = cur.next; cur.next = pre; temp.next = cur; temp = pre; } return ret.next; };
19、删除链表的倒数第N个节点
给你一个链表,删除链表的倒数第
n
个结点,并且返回链表的头结点。
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
输入:head = [1], n = 1
输出:[]
var removeNthFromEnd = function(head, n) { let ret = new ListNode(0, head), slow = fast = ret; while(n--) fast = fast.next; if(!fast) return ret.next; while (fast.next) { fast = fast.next; slow = slow.next }; slow.next = slow.next.next; return ret.next; };
面试题 02.07. 链表相交
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。
var getListLen = function(head) { let len = 0, cur = head; while(cur) { len++; cur = cur.next; } return len; } var getIntersectionNode = function(headA, headB) { let curA = headA,curB = headB, lenA = getListLen(headA), lenB = getListLen(headB); if(lenA < lenB) { [curA, curB] = [curB, curA]; [lenA, lenB] = [lenB, lenA]; } let i = lenA - lenB; while(i-- > 0) { curA = curA.next } while(curA && curA !== curB) { curA = curA.next; curB = curB.next; } return curA; };