1. 含义
双向链表(Doubly Linked List)是一种链表数据结构,它的每个节点都含有两个指针,一个指向前一个节点,一个指向后一个节点。相比较于单向链表,双向链表可以双向遍历,即可以从头到尾或从尾到头遍历链表。在双向链表中,每个节点包含两个指针域和一个数据域。其中,一个指针指向前驱节点,另一个指针指向后继节点。
2. 基本方法
1. size() : int //返回链表长度
2. isEmpty() : boolean // 判空
3. clear() : void //清除所有元素
4. contains(E element) : boolean //判断是否包含此元素
5. get(int index) : E //返回该下标处的元素
6. set(int index, E) : E //修改该下标处的元素
7. add(E element) : void //末尾添加元素
8. add(int index, E) : void //指定下标处添加元素
9. remove(int index) : E //移除指定下标处的元素
10. indexof(E element) : int //返回该元素的下标
3. 内部实现
1. Node节点设计
与单向链表相比增加了prev指针,指向上一个ji
private static class Node<E> {
// 存储元素
E element;
// 存储上一个节点
Node<E> prev;
// 存储下一个节点
Node<E> next;
// 构造方法
public Node(Node<E> prev, E element, Node<E> next) {
this.prev = prev;
this.element = element;
this.next = next;
}
}
2. 方法设计
1. size()
public int size() {
return size;
}
2. isEmpty()
public boolean isEmpty() {
return size == 0;
}
3. clear()
public void clear() {
size = 0;
// 首指针和尾指针均需为null
first = null;
last = null;
// 剩余节点对象因不被gc root对象所指会被回收
}
4. contains(E element)
public boolean contains(E element) {
// 返回下标如果不为常数-1则存在,反之不存在
return indexOf(element) != ELEMENT_NOT_FOUND;
}
5. get(int index)
public E get(int index) {
return node(index).element;
}
// 用于获取index位置对应的节点对象
private Node<E> node(int index) {
// 判断下标是否越界
rangeCheck(index);
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
6. set(int index, E)
public E set(int index, E element) {
Node<E> node = node(index);
E old = node.element;
node.element = element;
return old;
}
7. add(E element)
public void add(E element) {
add(size, element);
}
8. add(int index, E)
public void add(int index, E element) {
// 判断下标是否越界
rangeCheckForAdd(index);
if (index == size) { // 往最后面添加元素
Node<E> oldLast = last;
last = new Node<>(oldLast, element, null);
if (oldLast == null) { // 这是链表添加的第一个元素
first = last; //第一个和最后一个指向相同
} else {
oldLast.next = last;
}
} else {
// index位置的节点变成:添加节点的下一个节点
Node<E> next = node(index);
Node<E> prev = next.prev;
Node<E> node = new Node<>(prev, element, next);
next.prev = node;
if (prev == null) { // 第一个节点添加元素
first = node;
} else {
prev.next = node;
}
}
size++;
}
9. remove(int index)
public E remove(int index) {
rangeCheck(index);
Node<E> node = node(index);
Node<E> prev = node.prev;
Node<E> next = node.next;
if (prev == null) { // 移除头节点
first = next;
} else {
prev.next = next;
}
if (next == null) { // 移除尾节点
last = prev;
} else {
next.prev = prev;
}
size--;
return node.element;
}
10. indexof(E element)
public int indexOf(E element) {
if (element == null) {
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (node.element == null) return i;
node = node.next;
}
} else {
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (element.equals(node.element)) return i;
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
4. 练习题目
876. 链表的中间结点
给你单链表的头结点
head
,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:head = [1,2,3,4,5] 输出:[3,4,5] 解释:链表只有一个中间结点,值为 3 。示例 2:
输入:head = [1,2,3,4,5,6] 输出:[4,5,6] 解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点。
方法一:数组
链表的缺点在于不能通过下标访问对应的元素。因此我们可以考虑对链表进行遍历,同时将遍历到的元素依次放入数组 A 中。如果我们遍历到了 N 个元素,那么链表以及数组的长度也为 N,对应的中间节点即为 A[N/2]。
class Solution {
public ListNode middleNode(ListNode head) {
// 创建一个ListNode数组,提示最大容量为100
ListNode[] A = new ListNode[100];
// 记录head的节点数
int t = 0;
// 遍历所有节点
while (head != null) {
A[t++] = head;
head = head.next;
}
return A[t / 2];
}
}
方法二:单指针两次遍历
我们可以对链表进行两次遍历。第一次遍历时,我们统计链表中的元素个数 N;第二次遍历时,我们遍历到第 N/2 个元素(链表的首节点为第 0 个元素)时,将该元素返回即可。
class Solution {
public ListNode middleNode(ListNode head) {
int n = 0;
ListNode cur = head;
// 遍历链表得到节点数目n
while (cur != null) {
++n;
cur = cur.next;
}
int k = 0;
cur = head;
while (k < n / 2) {
++k;
cur = cur.next;
}
return cur;
}
}
方法三:快慢指针法
我们可以用两个指针 slow 与 fast 一起遍历链表。slow 一次走一步,fast 一次走两步。那么当 fast 到达链表的末尾时,slow 必然位于中间。
class Solution {
public ListNode middleNode(ListNode head) {
ListNode slow = head, fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}
160. 相交链表
给你两个单链表的头节点
headA
和headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回null
。图示两个链表在节点
c1
开始相交:题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):
intersectVal
- 相交的起始节点的值。如果不存在相交节点,这一值为0
listA
- 第一个链表listB
- 第二个链表skipA
- 在listA
中(从头节点开始)跳到交叉节点的节点数skipB
- 在listB
中(从头节点开始)跳到交叉节点的节点数评测系统将根据这些输入创建链式数据结构,并将两个头节点
headA
和headB
传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3 输出:Intersected at '8' 解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。 在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。 — 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
示例 2:
输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1 输出:Intersected at '2' 解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。 在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2 输出:null 解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。 由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。 这两个链表不相交,因此返回 null 。
分析
题解
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null) return null;
ListNode pA = headA, pB = headB;
// 两条链表加起来长度相同,最后都为null
while(pA != pB){
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}
21. 合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]示例 2:
输入:l1 = [], l2 = [] 输出:[]示例 3:
输入:l1 = [], l2 = [0] 输出:[0]
方法:递归
如果 l1 或者 l2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 l1 和 l2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。
题解
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if(list1 == null) return list2;
if(list2 == null) return list1;
if(list1.val < list2.val){ // 第一个链表头值小于第二个
list1.next = mergeTwoLists(list1.next,list2);
return list1;
} else{
list2.next = mergeTwoLists(list1,list2.next);
return list2;
}
}
}
2. 两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4] 输出:[7,0,8] 解释:342 + 465 = 807.示例 2:
输入:l1 = [0], l2 = [0] 输出:[0]示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] 输出:[8,9,9,9,0,0,0,1]
方法:预先指针
将两个链表看成是相同长度的进行遍历,如果一个链表较短则在前面补 0,比如 987 + 23 = 987 + 023 = 1010
每一位计算的同时需要考虑上一位的进位问题,而当前位计算结束后同样需要更新进位值
如果两个链表全部遍历完毕后,进位值为 1,则在新链表最前方添加节点 1
小技巧:对于链表问题,返回结果为头结点时,通常需要先初始化一个预先指针 pre,该指针的下一个节点指向真正的头结点 head。使用预先指针的目的在于链表初始化时无可用节点值,而且链表构造过程需要指针移动,进而会导致头指针丢失,无法返回结果。
题解
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 预先指针
ListNode dummy = new ListNode();
ListNode cur = dummy;
int carry = 0;
// 不能为.next != null
// 仅仅检查l1.next != null或l2.next != null中的一个
// 无法确保同时处理完两个链表的所有节点,最后一个处理不到
while(l1 != null || l2 != null) {
int x = l1 == null ? 0 : l1.val;
int y = l2 == null ? 0 : l2.val;
int sum = x + y + carry;
carry = sum / 10;
sum = sum % 10;
cur.next = new ListNode(sum);
cur = cur.next;
if(l1 != null) l1 = l1.next; // 下一个节点
if(l2 != null) l2 = l2.next;
}
if(carry == 1) cur.next = new ListNode(1);
return dummy.next;
}
19. 删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第
n
个结点,并且返回链表的头结点。示例 1:
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]示例 2:
输入:head = [1], n = 1 输出:[]示例 3:
输入:head = [1,2], n = 1 输出:[1]
方法:计算链表长度
首先从头节点开始对链表进行一次遍历,得到链表的长度 L。随后我们再从头节点开始对链表进行一次遍历,当遍历到第 L−n+1 个节点时,它就是我们需要删除的节点。
为了与题目中的 n 保持一致,节点的编号从 1 开始,头节点为编号 1 的节点。
为了方便删除操作,我们可以从哑节点开始遍历 L−n+1 个节点。当遍历到第 L−n+1 个节点时,它的下一个节点就是我们需要删除的节点,这样我们只需要修改一次指针,就能完成删除操作。
题解
public ListNode removeNthFromEnd(ListNode head, int n) {
// 设置虚拟头节点
ListNode dummy = new ListNode(0, head);
int length = getLength(head);
// 先指向虚拟头节点
ListNode cur = dummy;
// 开始遍历到要删除节点的前一个节点
for(int i=1; i <= length-n; i++) {
cur = cur.next;
}
// 将删除节点的前一个节点指针指向要删除节点的下一个节点
cur.next = cur.next.next;
return dummy.next;
}
// 计算链表长度的函数
public int getLength(ListNode head) {
int length = 0;
while(head != null) {
length++;
head = head.next;
}
return length;
}