用 JAVA 实现双向链表

08月04日 收藏 0 评论 5 java开发

用 JAVA 实现双向链表

转载声明:文章来源https://blog.csdn.net/u012780521/article/details/86671621

Java 虚拟机有一个垃圾回收机制,负责释放 / 回收无用对象所占用的内存空间,从而防止内存泄漏

(内存泄漏是指,程序中分配的堆内存未释放造成系统内存的浪费并引发一系列问题),并充分地利用内存。

虚拟机负责判断对象是否可以回收;

Java 语言规范没有规定虚拟机应该使用哪种垃圾回收算法;

程序员不能干涉虚拟机的内存管理。

1. 什么是链表

什么是铁路列车

铁路列车,是指在铁路轨道上行驶的车辆。通常由多节车厢组成。
相邻的车厢由它们之间的车钩连接。乘客载于车厢内部的座椅。

可以通过车钩在同一 铁路列车的不同车厢之间移动。

链表,是一种数据结构。通常由多个节点组成。

相邻的节点由它们之间的邻节点域连接。数据存储于数据域内。可以通过邻节点域访问同一链表中的不同节点 。

class Node {

Object data; // 车厢座椅————数据域
Node nextNode; // 车钩————邻节点域

Node(Object obj) {
data = obj;
}

public static void main(String[] args) {
Node node1 = new Node(new Object());
head.nextNode = new Node(new Object());
}
}

我们在上面的代码段中用 Node 对象来表示节点。

Node 类的 data 属性存储 Object 对象;这个属性是作为数据容器的部分,称为“数据域”;

nextNode 属性存储另一个 Node 对象,这个对象作为当前节点的“下一结点”;

这个属性是存储相邻节点的部分,称为“邻节点域”。

创建的两个 Node 对象分别在自己的数据域中存储了数据,并组成了通过邻节点域相连的链状结构,这就是链表。

我们没有为第二个 Node 对象创建一个独立的引用变量,因此,我们只能通过对象 head 的邻节点域来访问第二个节点。

我们只记录链表中的第一个节点,并通过各个节点的邻节点域访问每个节点。我们把用于访问链表的第一个节点叫做“首节点”。

实际上,nextNode 中存储的是下一节点对象的地址。当我们利用支持指针的语言来实现链表时,我们会把 “邻节点域 ”叫做“指针域 ” ;但是我们现在是用 Java 来实现链表,我们就把 “ 邻节点域 ” 叫做 “ 引用域 ” ——因为 nextNode 是下一节点对象的引用。

链表是最差的数据结构之一

它的各个节点在物理上是分散存储的;不像是数组,数组的各个元素在物理上是顺序地、相邻地存储的。

数组可以通过其首地址和索引来快速定位元素地址;而 链表只能通过引用域来查找和遍历。这个特点决定了 链表的查找和遍历效率极低 。

链表的每个节点都有它的引用域,这就要占用较多的空间。

链表在插入和删除上也许比较方便:类比列车的增减车厢,节点的增删只用改变两个邻节点域就行了。

但如果我们想在特定的位置插入 / 删除,就无法避免低效率的查找。

链表也有一些优点

比如,用链表可以很方便地实现队列(queue)。

上面的示例代码中的链表,查找和遍历只能通过一个方向。这一点和铁路列车不太一样:铁路列车可以按两个方向进行查找和遍历。

如果稍微改变一下上面的代码:

class Coach {

Object data; //数据域
Coach next; //引用域
Coach previous; //引用域
//...
}

上面的 Coach 类有 2 个引用域,分别存储上一节点和下一节点。

如果用 Coach 来代替上面的 Node ,就得到了一个可以双向查找的链式数据结构。

因为这种数据结构与铁路列车十分相似,所以它叫做“火车表”。 这就是 双向链表。代码段 1演示的链表就是 单向链表。

如果把双向链表的首节点和末节点通过引用域连接起来,就得到了 循环链表。

2. 定义节点类

上面的代码只是用来说明概念的。我们需要更加实用的代码:

class Node {

private Object data; //数据域
Node next; //引用域
Node previous; //引用域

public Node(Object data) {
this.data = data;
}

public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
}

单向链表和双向链表中都会有一个节点没有下一节点。

没有下一节点的节点叫做 末节点,它相当于火车最后一节车厢。

通常,我们让末节点的下一节点引用域的值为 null。

3. 定义链表类

public class MyLinkedList extends java.util.LinkedList {
}
public class LinkedList {

public Node head;
public Node tail; (注 1)
private int size;

public LinkedList() {
head = new Node(null, null); (注 2)
}

public int size() { return size; }

public void add(T obj) {
//...
}

public boolean add(T obj, int index) {
//...
}

public T remove(int index) {
//...
}
//...
}

注 1: 为什么需要一个 tail 变量?
便于在链表末尾添加节点。

注 2: 为什么要把 head 的数据域设为 null?
当我们创建一个新链表时,新链表里面没有存放任何数据,因此,只好把 head 节点的数据域设为null。
通常我们不向 head 中存放数据,只在 head 的引用域中存放 下一节点的地址 。
如果我们选择在 head 节点的数据域中存放数据……
那么,add() 方法就要辨别存入的数据是不是第一个元素。如果存入的是第一个元素,那就应该把数据存入 head 节点;如果存入的不是第一个元素,那就应该创建一个新的节点对象。

这样太麻烦了。所以,干脆就不往 head 的数据域中存放数据了。

4. 增加

在末尾增加,就是创建一个 Node 对象,然后把新创建的节点对象的地址存入 原来的 末节点的引用域。最后把新的节点作为末节点就行了。

public void add(T obj) {
(tail.next = new Node(obj)).previous = tail;
tail = tail.next;
size++;
}

不行。要考虑 tail 为 null 的情况。

public void add(T obj) {
if (tail == null) {
(head.next = tail = new Node(obj)).previous = head;
} else {
(tail.next = new Node(obj)).previous = tail;
tail = tail.next;
}
size++;
}

在定义添加方法时,要考虑到特殊的情况。

在指定位置添加元素 / 已有链表

向链表中插入已有链表时,要注意去除新增链表的 head 节点。

上面两种方法,在添加之前,都要先检查指定位置是否存在。然后要定位到指定位置。在创建新节点之后,都把新节点和已有节点连接起来。不妨先定义检查指定位置是否存在的方法、定位到指定位置的方法、连接两个节点的方法。

5. 检查、查找、连接、切除

1. 检查某位置是否存在

private boolean accessible(int index) { return index > -1 && index < size; }

2. 查找

private Node search(int index) {
if (accessible(index)) {
Node t = head.next;
for (int i = 0; i < index; i++) {
t = t.next;
}
return t;
}
return null;
}

3. 连接

private void link(Node front, Node back) {
if (front != null) {
front.next = back;
}
if (back != null) {
back.prevoius = front;
}
}

4. 切除

private void abandon(Node garbage) {
garbage.next = null;
garbage.previous = null;
}

为什么需要切除?

如果在删除节点时不调用 abandon() 方法断开旧节点与链表的连接,虚拟机看到弃用的节点与链表仍有联系,可能会误认为弃用的节点仍有用,并因此不回收它占用的内存空间。

5. 删除

删除指定位置的节点

删除指定区间的节点

删除含有指定数据的节点

上面三种方法,在删除之前,都要先检查指定位置是否存在。然后要定位到指定位置。在删除节点之后,把节点重新连接起来。

删除节点时,要调用 abandon() 方法来切断弃用的节点与链表的连接。

定义删除方法时,要注意特殊的情况,如删除末节点。

6. 单向链表的翻转

翻转很简单。只要往第 2 节点的引用域中存放第 1 节点就行了。

等等!如果往第 2 节点的引用域中存放第 1 节点,那怎么访问原来的第 3 节点呢?

因此,应该在往第 2 节点的引用域中存放第 1 节点之前,往第 3 节点的引用域中存放第 2 节点。

等等!如果往第 3 节点的引用域中存放第 2 节点,那怎么访问原来的第 4 节点呢?

因此,应该在往第 3 节点的引用域中存放第 2 节点之前,往第 4 节点的引用域中存放第 3 节点。

public static void reverse(LinkedList origin) {
Node t = origin.tail;
reverse(origin.tail = origin.head.next);
origin.head.next = t;
}

private static void reverse(Node iterator) {
if (iterator.next != null) {
Node _next = iterator.next;
reverse(_next);
_next.next = iterator;
iterator.next = null;
}
}

7. 我的代码

public class Node {

public Object data;
public Node next;
public Node previous;

public Node(Object obj) {
data = obj;
next = null;
previous = null;
}
}


public class LinkedList {

private int size;
public Node head;
public Node tail;

public LinkedList() {
this.head = new Node(null);
this.tail = new Node(null);
}

public int size() {
return size;
}

private boolean accessible(int index) {
return index > -1 && index < size;
}

private void link(Node front, Node back) {
if (front != null) {
front.next = back;
}
if (back != null) {
back.previous = front;
} else {
tail = front;
}
}

private void abandon(Node garbage) {
garbage.next = null;
garbage.previous = null;
}

public T get(int index) {
return (T) search(index).data;
}

public Node getNode(int index) {
return search(index);
}

private Node search(int index) {
if (accessible(index)) {
Node t = head.next;
while (index-- > 0) {
t = t.next;
}
return t;
}
return null;
}

public void add(T obj) {
Node _new = new Node(obj);
link(tail.data == null ? head : tail, _new);
tail = _new;
size++;
}

public T remove(int index) {
if (accessible(index)) {
Node previous = search(index - 1);
Node garbage = previous.next;
link(previous, garbage.next);
if (garbage == tail) {
tail = previous;
}
abandon(garbage);
size--;
return (T) garbage.data;
}
return null;
}

public boolean add(T obj, int index) {
if (index == size) {
add(obj);
return true;
}
if (accessible(index)) {
Node _new = new Node(obj), target = search(index), previous = target.previous;
link(previous, _new);
link(_new, target);
size++;
return true;
}
return false;
}

public boolean addList(LinkedList linkedList, int index) {
if (index == size) {
link(tail, linkedList.head.next);
tail = linkedList.tail;
size += linkedList.size;
return true;
}
if (accessible(index)) {
Node previous = search(index).previous, next = previous.next;
link(previous, linkedList.head.next);
link(linkedList.tail, next);
size += linkedList.size;
return true;
}
return false;
}

public int removeAll(T target) {
Node iterator = head;
int counter = 0;
while (iterator.next != null) {
iterator = iterator.next;
if (iterator.data.equals(target)) {
Node garbage = iterator;
link(iterator.previous, iterator.next);
iterator = iterator.previous;
if (garbage == tail) {
tail = garbage.previous;
}
abandon(garbage);
counter++;
}
}
size -= counter;
return counter;
}

public boolean remove(int left, int right) {
if (accessible(left) && accessible(right) && left <= right) {
Node leftNode = search(left).previous, rightNode = search(right).next;
if (right == size - 1) {
tail = leftNode;
}
abandon(leftNode.next);
abandon(rightNode.previous);
link(leftNode, rightNode);
size -= (right - left + 1);
return true;
}
return false;
}

public boolean change(T obj, int index) {
if (accessible(index)) {
Node target = search(index);
target.data = obj;
return true;
}
return false;
}

public int changeAll(T old, T intention) {
Node iterator = head;
int counter = 0;
while (iterator.next != null) {
iterator = iterator.next;
if (iterator.data.equals(old)) {
iterator.data = intention;
counter++;
}
}
return counter;
}

public String toString() {
StringBuffer sb = new StringBuffer();
Node t = head;
while (t.next != null) {
t = t.next;
if (t.data instanceof LinkedList) {
sb.append("> @");
sb.append(t.data.hashCode());
sb.append(" <\t");
} else {
sb.append(t.data + "\t");
}
}
return sb.toString();
}
}
C 5条回复 评论
三缄

大佬,能转载下吗?

发表于 2022-07-12 23:00:00
0 0
李子寒

我的java个人心得,入门重要,但是大多 数人都搞错了方向: 第一.切记不要一上来就找一大本厚书看。 这样你绝对会放弃。《Java核心技术》 《Java编程思想》 等都不适合入门阅读,很容易半途而废。 第二.先找一个入门级别的java教程看。 网上有很多极简入门教程。 例如runoob网站、w3cschool网站(它还有手机app) (上网搜一下关键词就有了)。 我记得我一开始入门找的教程,知识面全而精炼简洁, 含有基础、spring、Hibernate Servlet 等,地址如下仅供参考。 How2J 的 Java教程 第三.当你学完刚才那些网站之后, 你应该此时对java有了一个整体的认识, 那就去找一个小项目,GitHub很棒, https://github.com/上手练习,边做项目边查资料。 进步会飞快。 第四.这个阶段再回头精读一些java经典书籍。 获得内功上的提升。总之,一定要循序渐进, 一点点学才是最正确的选择。个人愚见,仅供参考

发表于 2021-11-10 22:00:00
0 0
地瓜土到掉渣

现在大二,希望自己能有坚定的觉悟和脚踏实地的努力

发表于 2021-09-29 21:00:00
0 0
黑加仑

大厂陆续开放校招了要抓紧时间

发表于 2021-09-13 17:10:00
0 0
wyj

放弃不难,但坚持一定很酷,加油,奥里给!

发表于 2021-09-09 12:20:00
0 0