数据结构与算法(二):单链表

一、什么是链表

链表是一种数据结构,跟数组不同,链表不需要连续的内存空间,而是通过指针将零散的内存块连接起来。

因此,链表的查找需要通过节点按顺序遍历,而增加与删除通过只需要操作指针指向,这也造成了相比数组,链表的查找性能消耗大增加和删除消耗小的特点。

链表由节点组成,一般每个节点最少包含用于储存数据的data区和用于指向下一个节点的指针next,有的链表可能会有头结点或尾结点用于存储链表长度等信息。

二、链表的简单实现

先来实现一个简单的节点类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* @Author:huang
* @Date:2020-06-20 10:19
* @Description:节点类
*/
public class Node {

//节点序号
int num;

//数据
Object data;

//下一个节点
Node next;

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

public int getNum() {
return num;
}

public Object getData() {
return data;
}

public void setNum(int num) {
this.num = num;
}

public void setData(Object data) {
this.data = data;
}

@Override
public String toString() {
return "Node{" +
"num=" + num +
", data=" + data +
'}';
}
}

1.插入节点

1.1不按照排序的插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @Author:huang
* @Date:2020-06-20 10:19
* @Description:单链表
*/
public class SingleLinkList {
//默认初始化一个头节点
private Node head = new Node(0, "我是头结点");

/**
* 添加节点到链表
* @param node
*/
public void add(Node node) {
Node temp = head;
//遍历链表
while (true) {
if (temp.next == null) {
break;
}
//不是尾节点就继续遍历下一个节点
temp = temp.next;
}
//将尾节点指向即将插入的新节点
temp.next = node;
}
}

1.2按照排序插入

要想按顺序插入节点,需要确定节点的位置,也就是确定新节点的大小,以图为例:

遍历链表,我们把遍历到的节点叫A,A之前的节点叫A-1,A之后的节点叫A+1,要插入的节点叫B,有以下几种情况:

  1. 当A的序号大于B时,就把B插到A前,也就是原本的A和A-1之间
  2. 当A的序号等于B时,抛出异常禁止插入
  3. 当A的序号小于B时,就跳过这个节点,继续遍历,然后对比A+1和B的大小
  4. 当A就是链表最后一个节点时,A还是小于B,那就直接让B插到A后,变成链表的尾节点

按这个思路,我们在原来的类里新增一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 按顺序添加节点到链表
* @param node 要插入的节点
*/
public void addByOrder(Node node) {
Node temp = head;
//遍历链表
while (true) {
//如果链表到底了就直接插入
if (temp.next == null) {
temp.next = node;
return;
}else if (temp.next.num > node.num){
//如果当前节点比当要插入的节点大,就把新节点插到当前节点之前
node.next = temp.next;
temp.next = node;
return;
}else if (temp.next.num == node.num){
//如果有节点序号和要插入的节点重复就抛异常
throw new RuntimeException("插入节点与已有节点序号重复!");
}
//如果当前节点比当要插入的节点小,就继续遍历
temp = temp.next;
}
}

2.查找节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 展示链表全部节点
*/
public void show() {
//判断链表是否为空
if (head.next == null) {
throw new RuntimeException("链表为空!");
}
Node temp = head.next;
//遍历链表
while (true) {
if (temp == null) {
break;
}
System.out.println(temp.toString());
temp = temp.next;
}
}

/**
* 根据序号获取节点
* @param num 要获取的节点序号
* @return
*/
public Node get(int num){
//判断链表是否为空
if (head.next == null) {
throw new RuntimeException("链表为空!");
}
Node temp = head.next;
//遍历链表
while (true) {
if (temp == null) {
throw new RuntimeException("编号为" + num + "的节点不存在!");
}
//如果是要获取的节点
if (temp.num == num) {
return temp;
}
temp = temp.next;
}
}

3.修改节点

修改节点与按顺序插入逻辑相似,需要先遍历并找到要修改的节点,然后用新节点去替换旧节点,或者干脆直接修改节点内的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 修改节点
* @param node 要更新的节点
*/
public void update(Node node) {
Node temp = head;
//判断链表是否为空
if (temp.next == null) {
throw new RuntimeException("链表为空!");
}
//获取要更新的节点序号
int nodeNum = node.num;
//遍历链表
while (true) {
//如果已经遍历完链表
if (temp == null) {
throw new RuntimeException("编号为" + temp.num + "的节点不存在!");
}
//如果找到了该节点
if (temp.num == nodeNum) {
temp.data = node.data;
return;
}
//继续遍历下一节点
temp = temp.next;
}
}

4.删除节点

因为在单向链表中,节点无法知道自己前一个节点的情况,以图为例:

如果我们想要删除节点A,那么就要找到A的前一个节点A-1,根据是否存在A后的节点A+1有以下几种情况:

  1. A节点就是链表尾节点,此时只需让A-1节点的next指向null即可

    nodeA-1.next = null

  2. A节点后有A+1,此时让A-1节点指向A+1节点

    nodeA-1.next = nodeA-1.next.next

按这个思路,在类里新加一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 删除节点
* @param num 要删除的节点编号
*/
public void delete(int num) {
Node temp = head;
//判断链表是否为空
if (temp.next == null) {
throw new RuntimeException("链表为空!");
}
//遍历链表
while (true) {
//如果链表到底了
if (temp.next == null) {
throw new RuntimeException("编号为" + num + "的节点不存在!");
}
//如果找到了待删除节点的前一个节点
if (temp.next.num == num) {
//判断待删除节点是否为尾节点
if (temp.next.next == null){
temp.next = null;
}else {
temp.next = temp.next.next;
}
return;
}
//继续遍历下一节点
temp = temp.next;
}
}

三、完整实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/**
* @Author:huang
* @Date:2020-06-20 10:19
* @Description:单链表
*/
public class SingleLinkList {

private Node head = new Node(0, "我是头结点");

/**
* 添加节点到链表
* @param node 要插入的节点
*/
public void add(Node node) {
Node temp = head;
//遍历链表
while (true) {
if (temp.next == null) {
break;
}
//不是尾节点就继续遍历下一个节点
temp = temp.next;
}
//将尾节点指向即将插入的新节点
temp.next = node;
}

/**
* 展示链表
*/
public void show() {
//判断链表是否为空
if (head.next == null) {
throw new RuntimeException("链表为空!");
}
Node temp = head.next;
//遍历链表
while (true) {
if (temp == null) {
break;
}
System.out.println(temp.toString());
temp = temp.next;
}
}

/**
* 根据序号获取节点
* @param num 要获取的节点序号
* @return
*/
public Node get(int num){
//判断链表是否为空
if (head.next == null) {
throw new RuntimeException("链表为空!");
}
Node temp = head.next;
//遍历链表
while (true) {
if (temp == null) {
throw new RuntimeException("编号为" + num + "的节点不存在!");
}
if (temp.num == num) {
return temp;
}
temp = temp.next;
}
}

/**
* 按顺序添加节点到链表
* @param node 要插入的节点
*/
public void addByOrder(Node node) {
Node temp = head;
//遍历链表
while (true) {
//如果链表到底了就直接插入
if (temp.next == null) {
temp.next = node;
return;
}
else if (temp.next.num > node.num){
//如果后一节点比当新节点大,就插入当前节点
node.next = temp.next;
temp.next = node;
return;
}else if (temp.next.num == node.num){
//如果后一节点等于新节点,抛异常
throw new RuntimeException("插入节点与已有节点序号重复!");
}
//如果后一节点比当前节点小,就继续遍历
temp = temp.next;
}
}

/**
* 修改节点
* @param node 要更新的节点
*/
public void update(Node node) {
Node temp = head;
//判断链表是否为空
if (temp.next == null) {
throw new RuntimeException("链表为空!");
}
//获取要更新的节点序号
int nodeNum = node.num;
//遍历链表
while (true) {
//如果已经遍历完链表
if (temp == null) {
throw new RuntimeException("编号为" + temp.num + "的节点不存在!");
}
//如果找到了该节点
if (temp.num == nodeNum) {
temp.data = node.data;
return;
}
//继续遍历下一节点
temp = temp.next;
}
}

/**
* 删除节点
* @param num 要删除的节点编号
*/
public void delete(int num) {
Node temp = head;
//判断链表是否为空
if (temp.next == null) {
throw new RuntimeException("链表为空!");
}
//遍历链表
while (true) {
//如果链表到底了
if (temp.next == null) {
throw new RuntimeException("编号为" + num + "的节点不存在!");
}
//如果找到了待删除节点的前一个节点
if (temp.next.num == num) {
//判断待删除节点是否为尾节点
if (temp.next.next == null){
temp.next = null;
}else {
temp.next = temp.next.next;
}
return;
}
//继续遍历下一节点
temp = temp.next;
}
}
}

四、环形链表

循环链表是一种特殊单链表,他跟单链表的区别在于尾节点的尾指针指向了链表的头结点,也就是相当于将链表连接成了一个环形。

环形链表可以用于处理能描述为环形的数据,典型的比如约瑟夫问题。

0%