单链表
数组和链表都是线性的数据存储结构的基础,栈和队列都是线性存储结构的应用。
众所周知,数组是一种连续的存储线性结构,元素的类型是相同的,大小相等。但是数组的存取速度快。不过好像数组的有点好像就只有这个,相反,数组的缺点就是一大摞:数组不能改变初始化后的大小,插入和删除元素效率低下,而且需要预先分配一定量的连续的内存。相比之下,链表是离散存储线性结构。每个单链表节点都只有两个指针(数据指针、下个节点的地址指针),彼此通过指针相连。。单链表的这些特性就决定了它不受磁盘空间限制,插入删除元素效率较高,唯一的缺点就是读写很慢。
- 单链表的首节点没有前驱节点,尾节点没有后续节点,首节点储存着下个节点的地址,下个节点也存储着下个节点的地址,依次类推,尾节点的下个节点地址为空。(详细可见下图)
基本操作
- 惯例,先创建一个节点类,并定义数据域和指针域:
/**
* 节点
* @author huaian
*
*/
public class Node {
// node data
int data;
// next node
Node nextNode;
// 构造函数
public Node(int data) {
super();
this.data = data;
}
}
- 单链表的基本操作有以下这几种:
方法名 | 描述 |
append() | 向链表的尾节点添加一个节点 |
getData() | 取出当前节点数据的值 |
isLast() | 判断是否是最后一个节点 |
remove(int index) | 删除指定索引的节点(index >= 1) |
1)追加节点
- 向链表的尾节点添加一个节点
– ① 先用最简单的方法来追加节点,简单到直接贴代码(为了方便操作,多谢一个 next() 方法来获取下一个节点):
/**
* 在末尾添加节点
* @param node 待添加节点
*/
public void append(Node node) {
this.nextNode = node;
}
/**
* 获取下一个节点
* @return 下一个节点
*/
public Node next() {
return this.nextNode;
}
– ② 追加节点的功能已经写完,现在调用一下:
// 创建一个节点
Node node01 = new Node(1);
// 追加节点 4 5 6
node01.append(new Node(4));
node01.next().append(new Node(5));
node01.next().next().append(new Node(6));
– ③通过上述代码我们可以看到,每次追加代码只能用过调用 next() 来获取下一个节点,非常麻烦。能不能 append() 自行判断下个节点是否为最后一个节点,上层调用只管追加就行?那肯定是可以,于是改了一下以上代码,见下:
/**
* 追加节点
* @param node 待添加的node
*/
public void append(Node node) {
// 获取当前节点
Node currentNode = this;
while (true) {
// 获取下一个节点
Node nextNode = currentNode.nextNode;
// 判断是否是最后一个节点
if (nextNode == null) {
break;
}
currentNode = nextNode;
}
// 添加节点
currentNode.nextNode = node;
}
– ④ 这样修改之后就可以这样添加元素,上层调用就只管 append ,无需获取下一个节点再添加节点。于是乎调用就变成了这样:
// 向 node01 添加元素
node01.append(new Node(4));
node01.append(new Node(5));
node01.append(new Node(6));
– ⑤ 其实我们还可以再改一下,使得可以链式调用 append(),把 append() 返回当前节点便可:
/**
* 追加节点
* @param node 待添加的node
*/
public Node append(Node node) {
// 获取当前节点
Node currentNode = this;
while (true) {
// 获取下一个节点
Node nextNode = currentNode.nextNode;
// 判断是否是最后一个节点
if (nextNode == null) {
break;
}
currentNode = nextNode;
}
// 添加节点
currentNode.nextNode = node;
return this;
}
– ⑥ 链式调用如下:
// 链式调用
node01.append(new Node(4))
.append(new Node(5))
.append(new Node(6));
- 那么,单链表添加节点的功能到这里就完成了。
2)获取数据
- 这个功能也比较简单,只需要把当前的数据域的值返回便可,这里就不累赘了,直接贴代码:
/**
* 获取当前节点的数据
* @return
*/
public int getData() {
return this.data;
}
3)判断是否是最后一个节点
- 这个功能也比较简单,只需要判断当前的节点的指针域是否为空便可,这里就不累赘了,直接贴代码:
/**
* 判断当前节点是否为空
* @return
*/
public boolean isLast() {
return nextNode == null;
}
4)删除某一节点
这个功能就有点绕了,成功的把自己也给绕进去了。不过还好憋出来了。
- 删除某一节点,核心思想就是:把存储当前节点的指针域,改为存储当前节点的下一个节点便可。
- 主要的难点是:因为单链表,当前节点只有下一个节点的指针域,无法获取前一个节点的信息。
- 实现思路:
– ① 获取当前链表和该链表的长度
– ② 找到待删除索引的下标的节点 current 的前一个节点 prev
– ③ 把 prev 的指针域链到 current 的下一个节点
– ④ 添加相关的有效性检查 - 代码实现:
/**
* 删除指定索引下标的元素
* @param index 索引
* @return 新的链表
*/
public Node remove(int index) {
Node headNode = this;
Node currentNode = headNode;
Node preNode = null;
// 索引越界或者当前节点不存在则抛出异常
if (index > size || index <= 0) {
throw new IndexOutOfBoundsException(index);
}
int count = 1;
// 找到待删除节点的前一个节点
while (!currentNode.isLast() && count < index) {
preNode = currentNode;
currentNode = currentNode.nextNode;
count++;
}
if (preNode == null && index == count) {
headNode = currentNode.nextNode;
} else {
preNode.nextNode = currentNode.nextNode;
}
size--;
return headNode;
}
完整代码
因为代码是演进的,可能在某个功能会出现阅读困难的问题,在这里贴上完整代码,方便查阅。
- 完整代码:
/**
* 节点
*
* @author huaian
*
*/
public class Node {
// node data
int data;
// next node
Node nextNode;
// node size
int size;
public Node() {
super();
size = 0;
}
public Node(int data) {
super();
this.data = data;
size++;
}
/**
* 追加节点
*
* @param node 待添加的node
*/
public Node append(Node node) {
// 获取当前节点
Node currentNode = this;
while (true) {
// 获取下一个节点
Node nextNode = currentNode.nextNode;
// 判断是否是最后一个节点
if (nextNode == null) {
break;
}
currentNode = nextNode;
}
// 添加节点
currentNode.nextNode = node;
size++;
return this;
}
/**
* 删除指定索引下标的元素
* @param index 索引
* @return 新的链表
*/
public Node remove(int index) {
Node headNode = this;
Node currentNode = headNode;
Node preNode = null;
// 索引越界或者当前节点不存在则抛出异常
if (index > size || index <= 0) {
throw new IndexOutOfBoundsException(index);
}
int count = 1;
// 找到待删除节点的前一个节点
while (!currentNode.isLast() && count < index) {
preNode = currentNode;
currentNode = currentNode.nextNode;
count++;
}
if (preNode == null && index == count) {
headNode = currentNode.nextNode;
} else {
preNode.nextNode = currentNode.nextNode;
}
size--;
return headNode;
}
/**
* 获取下一个节点
*
* @return 下一个节点
*/
public Node next() {
return this.nextNode;
}
/**
* 获取当前节点的数据
*
* @return 节点数据
*/
public int getData() {
return this.data;
}
/**
* 判断当前节点是否是最后一个节点
*
* @return 是否为最后一个节点
*/
public boolean isLast() {
return nextNode == null;
}
/**
* 链表节点数
*
* @return 节点数
*/
public int size() {
return size;
}
/**
* 判空
*
* @return 是否为空链表
*/
public boolean isEmpty() {
return size == 0;
}
}
总结
- 其实链表在数据结构的内容中还是占有很大的比重的,但是关乎于指针的东西还是比较复制,不过幸好我们用的是 Java 语言,在 C 语言中,实现上述代码,还是要有一定的理解能力的。有人说 Java 的数据结构是没有灵魂的(其实是我说的)。因为 Java 没有指针,C/C++ 才有指针。数据结构学的是思想,锻炼的是思维,与语言的关系不大,所以关于语言这一块,可以按照个人喜好来选择。
- 本来这篇文章还要憋很久才能出来,但是!!!今天(20190922)情况特殊,打个篮球还把脚给崴了,得去敷药,今天就写到这吧。后续会慢慢补上。
- 20190923 更,下课回来继续补充,不知道是怎么滴,浑身痛,躺了两个小时还是爬起来写,结果 remove 有点绕,结果把自己给绕进去了。后面问了下同学,捋了一下思路,还是憋出来了。
觉得不错,请留下你们的赞。谢谢。如果有任何问题可以,私信我,欢迎学习交流。
- 惯例放一下座右铭:
人若无名,专心练剑!