[Java] 双向链表

 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;
	}

  • 28
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中实现双向链表需要定义一个节点类,节点类中包含一个数据项和两个指针,分别指向前一个节点和后一个节点。下面是一个简单的Java代码示例: ```java class Node { int data; Node prev; Node next; public Node(int data) { this.data = data; this.prev = null; this.next = null; } } class DoublyLinkedList { Node head; public DoublyLinkedList() { this.head = null; } // 在链表头部插入一个节点 public void insertAtHead(int data) { Node newNode = new Node(data); newNode.next = head; if (head != null) { head.prev = newNode; } head = newNode; } // 在链表尾部插入一个节点 public void insertAtTail(int data) { Node newNode = new Node(data); if (head == null) { head = newNode; return; } Node current = head; while (current.next != null) { current = current.next; } current.next = newNode; newNode.prev = current; } // 在指定位置插入一个节点 public void insertAtPosition(int data, int position) { Node newNode = new Node(data); if (head == null || position == 1) { newNode.next = head; if (head != null) { head.prev = newNode; } head = newNode; return; } Node current = head; int currentPosition = 1; while (currentPosition < position - 1 && current.next != null) { current = current.next; currentPosition++; } newNode.next = current.next; newNode.prev = current; current.next = newNode; if (newNode.next != null) { newNode.next.prev = newNode; } } // 从链表中删除一个节点 public void deleteNode(int data) { if (head == null) { return; } Node current = head; while (current != null) { if (current.data == data) { if (current.prev != null) { current.prev.next = current.next; } else { head = current.next; } if (current.next != null) { current.next.prev = current.prev; } return; } current = current.next; } } // 输出链表中的所有节点 public void display() { Node current = head; while (current != null) { System.out.print(current.data + " "); current = current.next; } System.out.println(); } } ``` 然后可以通过以下方式进行测试: ```java public class Main { public static void main(String[] args) { DoublyLinkedList list = new DoublyLinkedList(); list.insertAtHead(3); list.insertAtHead(2); list.insertAtHead(1); list.insertAtTail(4); list.insertAtTail(5); list.insertAtPosition(6, 3); list.display(); // 输出:1 2 6 3 4 5 list.deleteNode(3); list.display(); // 输出:1 2 6 4 5 } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值