双向链表在需要双向遍历、频繁删除已知节点或实现撤销/重做等场景下优于单向链表,1. 当需支持前后导航(如浏览器历史、播放列表)时,双向链表可高效实现;2. 当需o(1)删除已知节点(如lru缓存)时,prev指针避免了遍历查找前驱;3. 当实现复杂数据结构或操作历史管理时,双向链接提供了灵活的节点关系维护;尽管其内存开销略高且逻辑更复杂,但在上述场景中性能和便利性优势显著,因此应根据具体需求权衡选择。
在Java中实现双向链表,核心在于定义一个包含数据、前驱节点和后继节点的内部类,然后构建链表类来管理这些节点,确保每个操作(如添加、删除)都正确更新前驱(
prev
next
实现一个双向链表,我们通常需要两个主要部分:
Node
DoublyLinkedList
1. Node
prev
next
class Node { int data; Node prev; Node next; public Node(int data) { this.data = data; this.prev = null; // 初始时,前驱和后继都为空 this.next = null; } }
2. DoublyLinkedList
head
tail
public class DoublyLinkedList { private Node head; private Node tail; private int size; // 维护链表大小,方便快速获取 public DoublyLinkedList() { this.head = null; this.tail = null; this.size = 0; } // 添加到链表头部 public void addFirst(int data) { Node newNode = new Node(data); if (head == null) { // 链表为空 head = newNode; tail = newNode; } else { newNode.next = head; // 新节点的next指向原head head.prev = newNode; // 原head的prev指向新节点 head = newNode; // 更新head为新节点 } size++; // System.out.println("Added " + data + " to head. Current size: " + size); } // 添加到链表尾部 public void addLast(int data) { Node newNode = new Node(data); if (tail == null) { // 链表为空 head = newNode; tail = newNode; } else { tail.next = newNode; // 原tail的next指向新节点 newNode.prev = tail; // 新节点的prev指向原tail tail = newNode; // 更新tail为新节点 } size++; // System.out.println("Added " + data + " to tail. Current size: " + size); } // 在指定索引处添加(这里只是一个简单的示例,实际可能需要更多边界检查) public void addAtIndex(int index, int data) { if (index < 0 || index > size) { throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); } if (index == 0) { addFirst(data); return; } if (index == size) { addLast(data); return; } Node newNode = new Node(data); Node current = head; // 遍历到目标位置的前一个节点 for (int i = 0; i < index - 1; i++) { current = current.next; } // current现在是新节点的前一个节点 newNode.next = current.next; newNode.prev = current; current.next.prev = newNode; // 原来current.next的prev指向新节点 current.next = newNode; // current的next指向新节点 size++; // System.out.println("Added " + data + " at index " + index + ". Current size: " + size); } // 删除链表头部 public int removeFirst() { if (head == null) { throw new IllegalStateException("List is empty."); } int data = head.data; if (head == tail) { // 只有一个节点 head = null; tail = null; } else { head = head.next; // head指向下一个节点 if (head != null) { // 新的head的prev应该为null head.prev = null; } } size--; // System.out.println("Removed " + data + " from head. Current size: " + size); return data; } // 删除链表尾部 public int removeLast() { if (tail == null) { throw new IllegalStateException("List is empty."); } int data = tail.data; if (head == tail) { // 只有一个节点 head = null; tail = null; } else { tail = tail.prev; // tail指向前一个节点 if (tail != null) { // 新的tail的next应该为null tail.next = null; } } size--; // System.out.println("Removed " + data + " from tail. Current size: " + size); return data; } // 删除指定索引处的节点 public int removeAtIndex(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); } if (index == 0) { return removeFirst(); } if (index == size - 1) { return removeLast(); } Node current = head; // 遍历到目标节点 for (int i = 0; i < index; i++) { current = current.next; } // current现在是需要删除的节点 int data = current.data; current.prev.next = current.next; // 前一个节点的next指向删除节点的next current.next.prev = current.prev; // 后一个节点的prev指向删除节点的prev size--; // System.out.println("Removed " + data + " at index " + index + ". Current size: " + size); return data; } // 从头到尾遍历 public void displayForward() { if (head == null) { System.out.println("List is empty."); return; } Node current = head; System.out.print("Forward: "); while (current != null) { System.out.print(current.data + " "); current = current.next; } System.out.println(); } // 从尾到头遍历 public void displayBackward() { if (tail == null) { System.out.println("List is empty."); return; } Node current = tail; System.out.print("Backward: "); while (current != null) { System.out.print(current.data + " "); current = current.prev; } System.out.println(); } public int getSize() { return size; } public boolean isEmpty() { return size == 0; } }
示例用法:
立即学习“Java免费学习笔记(深入)”;
public class DoublyLinkedListDemo { public static void main(String[] args) { DoublyLinkedList list = new DoublyLinkedList(); System.out.println("Is list empty? " + list.isEmpty()); // true list.addLast(10); list.addFirst(5); list.addLast(20); list.addAtIndex(1, 15); // 链表: 5 -> 15 -> 10 -> 20 list.displayForward(); // Output: Forward: 5 15 10 20 list.displayBackward(); // Output: Backward: 20 10 15 5 System.out.println("List size: " + list.getSize()); // 4 list.removeFirst(); // 移除 5 list.displayForward(); // Output: Forward: 15 10 20 list.removeLast(); // 移除 20 list.displayForward(); // Output: Forward: 15 10 list.removeAtIndex(0); // 移除 15 list.displayForward(); // Output: Forward: 10 System.out.println("List size: " + list.getSize()); // 1 list.removeFirst(); // 移除 10 list.displayForward(); // Output: List is empty. System.out.println("Is list empty? " + list.isEmpty()); // true } }
说实话,很多人一开始接触链表,可能觉得单向链表就够用了,毕竟它更简单,内存开销也小一点点。但实际开发中,双向链表的存在感是相当强的,它解决了一些单向链表“力所不能及”的痛点。
最直观的区别在于遍历方向。单向链表只能从头到尾,像一条单行道,一旦走过就回不去了。但双向链表,顾名思义,可以双向通行。这意味着,如果你需要频繁地“回溯”操作,比如浏览器历史记录(前进/后退)、文本编辑器的撤销/重做功能,或者像LRU缓存那样,需要快速将某个访问过的节点移动到链表头部,双向链表就显得非常自然且高效。
另一个关键点在于删除操作。在单向链表中,要删除一个节点,你必须找到它的“前一个”节点,然后修改那个前一个节点的
next
prev
node.prev
node.next
那么,代价是什么?内存。每个节点多了一个
prev
所以,选择哪个,真的要看具体需求。如果你的应用场景只需要顺序遍历,或者内存极其敏感,单向链表可能是更好的选择。但如果需要灵活的双向遍历,或者频繁地在已知节点位置进行删除操作,那么双向链表的优势就非常明显了,它带来的便利性和性能提升,往往能抵消那一点点内存和复杂度的增加。
在Java里写双向链表,最让人头疼的不是核心逻辑,而是那些细碎的边缘情况和随之而来的空指针异常(
NullPointerException
首先,空链表是所有操作的起点和终点。当链表是空的(
head == null
tail == null
addFirst
addLast
head
tail
removeFirst
removeLast
IllegalStateException
其次,单节点链表也是一个需要单独考虑的特殊情况。当链表中只有一个节点时,
head
tail
head
tail
null
head.next
tail.prev
head.next
null
NullPointerException
再来,prev
next
prev
next
addFirst
next
head
head
prev
head
null
next
prev
head
tail
head
tail
prev
next
null
具体到代码层面,这通常意味着大量的
if (node != null)
removeFirst()
head
null
head.prev = null
if (head != null)
addAtIndex
removeAtIndex
index < 0
index > size
一个好的实践是,将这些边缘情况的判断放在方法的开头,快速处理并返回或抛出异常。这样可以避免后续的复杂逻辑被这些特殊情况污染,让代码更清晰。同时,对
head
tail
null
双向链表节点关系的维护,不仅仅是实现层面的技术细节,它更是决定了某些特定应用场景下数据结构选择的关键。当我们需要高效地执行以下操作时,双向链表的优势就会被放大:
1. 频繁的就近操作或上下文感知: 想象一下,你在一个播放器里,需要实现“上一曲”和“下一曲”的功能。如果用单向链表,实现“下一曲”很容易,但“上一曲”就麻烦了,你可能需要从头遍历才能找到当前歌曲的前一首。双向链表则天然支持这种双向导航,每个节点都知道它的“邻居”,操作效率极高。这同样适用于浏览器历史记录,你可以轻松地“回退”到上一个页面,或者“前进”到下一个页面。
2. 快速删除已知节点: 在某些缓存机制中,比如LRU(Least Recently Used)缓存,当一个数据被访问时,它需要被移动到链表的“最新”位置(比如头部)。当缓存满时,需要删除“最旧”的数据(比如尾部)。更复杂的是,如果某个数据在链表中间被访问了,它需要从中间被“取出”,然后放到头部。在双向链表中,由于每个节点都知道它的前驱和后继,只要拿到了这个节点的引用,删除它就变成了 O(1) 的操作:让它的前驱跳过它指向它的后继,让它的后继跳过它指向它的前驱。这种效率在高性能要求的缓存系统中至关重要。
3. 实现复杂数据结构的基础: 很多高级数据结构,比如一些复杂的图算法实现,或者某些自定义的内存管理系统,会使用双向链表作为其底层构建块。因为在这些场景下,数据元素之间的关系可能不是简单的线性顺序,但需要快速地在相邻元素之间跳转,甚至需要将某个元素从一个位置“剪切”到另一个位置,双向链表的灵活性提供了很好的支持。
4. 撤销/重做(Undo/Redo)功能: 在文本编辑器、图形设计软件等应用中,撤销和重做功能是不可或缺的。每一次操作都可以看作是链表中的一个节点。撤销就是沿着
prev
next
总的来说,当你的应用场景需要超越简单的线性遍历,涉及到对数据元素的“位置感知”、“双向导航”或“快速插入/删除”时,双向链表就是那个值得你投入精力去维护节点关系的选择。它带来的便利性和性能提升,往往能让你的系统设计更加优雅和高效。
以上就是java代码如何实现双向链表并处理节点关系 java代码双向链表的基础编写教程的详细内容,更多请关注php中文网其它相关文章!
java怎么学习?java怎么入门?java在哪学?java怎么学才快?不用担心,这里为大家提供了java速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!
Copyright 2014-2025 //m.sbmmt.com/ All Rights Reserved | php.cn | 湘ICP备2023035733号