文章目录
1. 什么是链表
链表是一种物理存储结构上非连续的存储结构,是线性表的一种。
使用链表的原因:链表和顺序表同属于一种线性表但是顺序表有扩容浪费空间,以及中间位置插入元素和删除元素需要移动大量的数据效率低的缺点,使用链表则可以解决这些问题。
2. 链表的种类
- 单向,双向
- 带头,不带头
- 循环,非循环
重点掌握:无头单向非循环链表,一般是配合其他数据结构使用,如哈希桶,图等等;另一个就是无头双向链表。
3. 创建单链表
单链表由两个类组成,一个类用来存储单链表中节点的元素以及指向下一个节点的地址,相当于现实社会中的火车车厢,每节车厢通过铁链相连,铁链相当于指向下一个节点的地址,只有知道地址才能使两个节点相连。每节车厢存放不同的物品就是节点中的元素。有了车厢才能组成一辆火车,所以另一个类就是火车类,存储的是单链表中实际存储的元素和单链表的头节点。
3.1 创建 Node 类(车厢类)
/**
* 车厢类
*/
public class Node {
// 存储具体元素
int data;
// 存储下一个节点的地址
Node next;
public Node(int data) {
this.data = data;
}
public Node(int data, Node next) {
this.data = data;
this.next = next;
}
}
3.2 创建 MySingleList (火车类)
public class MySingleList {
// 链表中的实际存储元素
private int size;
// 链表中的头节点
private Node head;
}
4. 单链表的基本操作
4.1 增加链表元素
4.1.1 在链表头部添加元素
public void firstAdd(int data) {
// 关注临界条件
// 1.链表中有没有节点,判空
// 2.链表中已经有节点了
if (size == 0) {
// 1. 先创建一个新节点存放 data 值
Node node = new Node(data);
// 2. 头节点变为新加入的节点
head = node;
size++;
} else {
// 1. 先创建一个新节点存放 data 值
Node node = new Node(data);
// 2. 将新节点与原来的第一个节点连接
node.next = head;
// 3. 将头节点变为新的节点
head = node;
size++;
}
}
图解:
4.1.2 在链表任意位置添加元素
在 index 位置插入新节点要先找到前驱节点,为什么找前驱节点,因为单链表特点是只能从前往后遍历,所以要在任意位置添加节点要先知道它的前驱节点。添加步骤:先要使新节点与原来该位置的节点相连,并且将原来的前驱节点与新节点相连,这样就添加成功了。
public void indexAdd(int index, int data) {
// 边界条件:1. 链表是否为空 2. 索引是否合法 3. index 是否是第一个节点
// 1. 链表是否为空
if (size == 0) {
System.err.println("链表为空!");
} else if (index < 0 || index > size) { //2. 索引是否合法
System.err.println("索引非法!");
} else if (index == 0) { // 3. index 是否是第一个节点
firstAdd(data);
} else {
Node node = new Node(data);
// 前驱节点
Node prev = head;
// 遍历找到 index 的前驱节点
for (int i = 0; i < index - 1; i++) {
prev = prev.next;
}
// 先将 index 位置的节点与插入的节点连接,index 位置的节点就是 prev.next
node.next = prev.next;
// 再将插入的节点与前驱节点连接
prev.next = node;
size++;
}
}
图解:
4.1.3 在链表尾部添加元素
public void lastAdd(int data) {
indexAdd(size,data);
size++;
}
4.2 删除链表元素
在单链表中遇到 index 都需要判断索引是否合法,所以可以将判断索引是否合法这个方法提出来。
// 索引是否合法
private boolean rangeCheck(int index) {
if (index < 0 || index >= size) {
System.err.println("索引非法!");
return false;
}
return true;
}
4.2.1 删除第一个节点元素
public void deleteFirst() {
// 创建一个临时变量存放 head
Node node = head;
// 将头节点从第一个变为第二个
head = head.next;
// 将原来的第一个节点断开
node.next = null;
size--;
}
图解:
4.2.2 删除任意位置 index 元素
public void deleteIndex(int index) {
// 边界条件:1. 链表为空 2. 索引合法 3. 删除的是第一个节点
// 1. 链表为空
if (size == 0) {
System.out.println("链表为空!");
// 2. 索引合法
} else if (rangeCheck(index)) {
// 3. 删除的是第一个节点
if (index == 0) {
deleteFirst();
} else {
// 暂存 head 节点
Node prev = head;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
// prev 指向待删除节点的前驱节点
// node 就是待删除节点
Node node = prev.next;
// 链接前驱节点和后继节点
prev.next = node.next;
// 将当前 node 节点的 next 置为空,断开待删除元素和链表的链接
node.next = null;
size--;
}
}
}
图解:
4.2.3 删除链表中指定元素的第一个节点
public void deleteDataOnce(int data) {
if (size == 0) {
System.out.println("链表为空!");
} else {
// 临界条件:指定元素在头节点
if (head.data == data) {
deleteFirst();
} else {
Node prev = head;
while (prev.next != null) {
// 如果 prev.next 是指定元素
if (prev.next.data == data) {
// 创建 node 放指定元素的节点
Node node = prev.next;
// 将 prev.next 指向 prev.next.next,跳过 node
prev.next = node.next;
// 将 node 与链表断开
node.next = null;
size--;
break;
} else {
// 如果 prev.next 不是指定元素,继续判断下一个
prev = prev.next;
}
}
}
}
}
图解:
4.2.4 删除链表的所有指定元素
public void deleteAll(int data) {
// 临界条件:指定元素是第一个节点
// head != null 链表中的元素都相同,如果一直删除会出现链表为空,不能继续往下判断
while (head != null && head.data == data) {
deleteFirst();
}
// 链表都是待删除的节点且已经被删除完了
if (head == null) {
return;
} else {
Node prev = head;
while (prev.next != null) {
if (prev.next.data == data) {
// node 就是待删除节点
Node node = prev.next;
prev.next = node.next;
node.next = null;
size--;
} else {
prev = prev.next;
}
}
}
}
图解:
4.3 查找链表元素
4.3.1 判断链表中是否包含元素 data
public boolean contains(int data) {
Node node = head;
while (node != null) {
if(node.data == data) {
// System.out.println("链表中包含该元素");
return true;
}
node = node.next;
}
// System.out.println("链表中没有该元素");
return false;
}
4.3.2 得到链表中 index 位置的元素
public int get(int index) {
if(rangeCheck(index)) {
Node node = head;
for (int i = 0; i < index; i++) {
node = node.next;
}
// node.data 就是 index 位置的元素
int data = node.data;
return data;
}else{
// 索引不合法
return -1;
}
}
4.4 修改链表元素
public void set(int index, int data) {
if(rangeCheck(index)) {
Node node = head;
for (int i = 0; i < index; i++) {
node = node.next;
}
// 将原来 index 位置的元素修改成 data
node.data = data;
} else{
System.out.println("索引非法");
}
}
4.5 打印链表
public String toString() {
String result = "";
// 创建一个新节点存放头节点防止遍历完链表找不到链表的头
Node node = head;
while (node != null) {
result += node.data + "->";
node = node.next;
}
// 如果链表为空,引用类型的默认值为 null
result += "null";
return result;
}
总结:1. 链表中无论是添加还是删除操作如果参数中有索引,都需要注意索引是不是链表中的头节点,因为头节点比较特殊,它没有前驱节点。2. 使用索引时还需要注意索引是否合法即 index < 0 || index >= size 索引不合法。这点与之前顺序表有所不同,因为顺序表可以在最后扩容所以 index 可以等于 size。