浅谈数据结构之链表 | OkyCode 

JerryXia 发表于 , 阅读 (0)

什么是链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表由一系列结点(元素)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域

Alt Image Text

相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

牺牲随机访问,换取快速插入和删除。

单向链表

单向链表又称单链表,是链表的一种,链表的链接方向是单向的,对单链表的访问要通过顺序读取从头部开始。

Alt Image Text
一个完整的单向链表包含一个head头节点,和一个tail尾节点,而尾节点的next指针域指向一个null空对象。好了,了解了一个单向链表的内部结构后,下面我门来创建一个我们自己的单链表。

首先,创建一个内部类,实例代表一个链表结点。

1
2
3
4
5
6
7
8
9
10
public final class Node {
private T data;//存储数据,数据域
private Node next;//指向下一个结点的引用,指针域

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

}

定义三个成员变量header、tail和size,分别用于保存头节点、尾节点和链表的节点数

1
2
3
private Node header = null;//用于保存链表头结点
private Node tail = null;//用于保存链表尾节点
private int size = 0;//保存链表节点数

添加

简单构建一个添加的方法,添加前我门首先要判断其是否是一个空的链表,如果header == null即为一个空链表,创建一个结点作为header头节点,让tail尾节点指向新建结点的引用,注意这里的指向是指tail尾节点指向而不是tail尾节点的next指针域指向
Alt Image Text

如果header != null,创建一个新结点,尾节点next指针指向新节点的引用,让新节点作为尾节点使用。
Alt Image Text

在尾部添加元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 添加一条数据,默认在尾部插入
*
* @param element
*/

public void add(T element) {
if (header == null) {
//如果是一个空链表
header = new Node(element, tail);
tail = header;
} else {
//创建一个新节点
Node node = new Node(element, null);
//尾节点指向新节点的引用
//值得注意的是这里的next是哪个引用的
tail.next = node;
//新节点作为尾节点使用
tail = node;
}
size++;
}

删除

由于其链式的数据结构,删除时我们只需要改变要删除结点前置结点next指针指向的引用即可,然后置空要删除结点next的指针域。这就是为什么相对于其他线性表插入和删除较快的原因,插入和删除时我们不需要重新移位其他数据。
Alt Image Text
具体代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 根据索引删除节点
*
* @param index
* @return
*/

public T delete(int index) {
checkElementIndex(index);
Node delNode = null;
if (index == 0) {
//如果清除的是头节点
delNode = header;
header = header.next;
} else {
//获取索引位置结点的前置结点
Node prevNode = getNodeByIndex(index - 1);
delNode = prevNode.next;
prevNode.next = delNode.next;
delNode.next = null;
}
size--;
return delNode.data;
}

查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 获取索引处的数据
*
* @param index
* @return
*/


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

private Node getNodeByIndex(int index) {
Node x = header;
for (int i = 0; i < index; i++)
x = x.next;
return x;
}

下面附上一份完整的代码

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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/**
* Created by okycode
* <p>
* Description
*/


public class SimpleLinkedList<T> {
private Node header = null;//用于保存链表头结点
private Node tail = null;//用于保存链表尾节点
private int size = 0;//保存链表节点数

/**
* 定义一个链表结点内部类
*/

public final class Node {
private T data;//节点数据
private Node next;//指向下一个节点的引用

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

}

/**
* 返回链表长度
*
* @return
*/

public int size() {
return size;
}

/**
* 获取索引处的数据
*
* @param index
* @return
*/

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

/**
* 添加一条数据,默认在尾部插入
*
* @param element
*/

public void add(T element) {
if (header == null) {
//如果是一个空链表
header = new Node(element, tail);
tail = header;
} else {
//创建一个新节点
Node node = new Node(element, null);
//尾节点指向新节点的引用
//这里值得注意的是next是哪个引用的
tail.next = node;
//新节点作为尾节点使用
tail = node;
}
size++;
}

/**
* 在任意位置插入一条数据
*
* @param index
* @param element
*/

public void add(T element, int index) {
checkElementIndex(index);
if (header == null) {
add(element);
} else {
if (index == 0) {
addFirst(element);
} else {
//获取前置索引位置的结点
Node prevNode = getNodeByIndex(index - 1);
//更改前置索引位置结点的指针指向新结点的引用
//新结点指针指向索引位置结点引用即可
prevNode.next = new Node(element, prevNode.next);
}

}
size++;
}

/**
* 根据索引删除节点
*
* @param index
* @return
*/

public T delete(int index) {
checkElementIndex(index);
Node delNode = null;
if (index == 0) {
//如果清除的是头节点
delNode = header;
header = header.next;
} else {
//获取索引位置结点的前置结点
Node prevNode = getNodeByIndex(index - 1);
delNode = prevNode.next;
prevNode.next = delNode.next;
delNode.next = null;
}
size--;
return delNode.data;
}

/**
* 清除链表
*/

public void clear() {
header = null;
tail = null;
size = 0;
}

/**
* 在头部添加一条数据,效率高相比于尾插
*
* @param element
*/

public void addFirst(T element) {
//创建一个新节点,让结点指针next指向头节点
Node node = new Node(element, header);
header = node;
//如果添加前是一个空链表
if (tail == null) {
tail = header;
}
size++;
}

private Node getNodeByIndex(int index) {
Node x = header;
for (int i = 0; i < index; i++)
x = x.next;
return x;
}


private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private String outOfBoundsMsg(int index) {
return "Index: " + index + ", Size: " + size;
}

private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder("[");
for (Node current = header; current != null; current = current.next) {
sb.append(current.data.toString() + ",");
}
int len = sb.length();
return sb.delete(len - 2, len).append("]").toString();
}
}

如何反转一个单链表

详细见代码注解,这里值得注意的是,我门在反转结点指针时要临时纪录结点后继结点防止断链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void reverse(SimpleLinkedList sll) {
if (sll == null) return;
Node pre = sll.header;// 头节点,上一结点
Node cur = sll.header.next;// 当前结点
Node tmp;// 临时结点,用于保存当前结点的指针域

while (cur != null) {// 当前结点为null,说明位于尾结点
tmp = cur.next;
cur.next = pre;// 反转指针域的指向
// 指针往下移动
pre = cur;
cur = tmp;
}
// 将原链表头节点置空作为尾节点
sll.header.next = null;