即使骑的小电驴,也要奋力前进
目录
1.链表
1.1 链表的概念
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的
链表的特点:①空间上,按需给空间 ②不要求物理空间连续,头部和中间的插入删除,不需要挪动数据
1.2 链表的逻辑结构图和物理结构图
1.2.1 链表的逻辑结构图
通过以上的逻辑结构图,我们可以看出链表是由结点组成,每个结点都包括两部分:数据域和指针域,通过指针域指向下一个结点来完成结点与结点之间的相连
- 数据域:用来存储数据
- 指针域:用来指向下一个结点
1.2.2 链表的物理结构图
通过以上的物理结构图,我们可以看到除了最后一个结点的指针指向null,其余的指针域都存储下一个结点的地址,所以指针域是用来存储下一个结点的地址来完成链接的
1.3链表结构的分类
1.3.1 链表通过什么进行结构的分类
链表通过以下表格中的六种情况组合起来进行结构的分类:
单向 | 双向 |
带头 | 不带头 |
循环 | 非循环 |
可以分为以下八种结构:
单向不带头循环链表 |
单向不带头非循环链表 |
单向带头循环链表 |
单向不带头循环链表 |
双向不带头循环链表 |
双向不带头非循环链表 |
双向带头循环链表 |
双向带头非循环链表 |
1.3.2 不同链表结构的逻辑图
①单向
单向:只要一个指针域存储下一个结点的地址
②双向
双向:有两个指针域,前指针域是用来存储前一个结点的地址,后指针域是用来存储后一个结点地址
③带头
带头:有一个哨兵位结点(head),数据域一般给一个无效值,当做头结点
④不带头
不带头:没有哨兵位结点
⑤循环
循环:最后一个结点没有指向空而是指向了第一个结点
⑥非循环
非循环:最后一个结点直接指向了空
2.模拟实现一个单向链表
现在我们知道了什么是链表,以及链表的分类,那么接下来我们就来模拟实现一个存放整型数据的单向不带头非循环链表
我们首先需要创建一个类来实现这样一个链表:
public class MyLinkedList {
}
接下来我们就需要将这个实现链表的过程全部放在 MyLinkedList 这个类中
2.1 MyLinkedList类的内部类
我们知道链表是用结点来存储数据的,并且还是用结点来实现结点与结点之间的链接
那我们现在就需要在 MyLinkedList类 中创建一个内部类当做结点类
private class Node {
private int data;//数据域
private Node next;//链接域
private Node(int data) {
this.data = data;
}
}
- data:用来存放数据
- next:用来存储下一个结点的地址,从而达到结点与结点之间的链接
- 构造方法:每实例化一个结点类时,都需要调用构造方法,来把数据放入数据域
2.2 MyLinkedList类的成员属性
我们需要给第一个结点设置为头结点,这样用户才知道链表应该从哪里开始,这样单链表才不会因为没有被指向而被回收
private Node head;//记录第一个结点
注:只要第一个结点没有被回收,其他结点也不会被回收,因为前一个结点的 next 存储着后一个结点的地址,这样前一个结点的指针域就指向了后一个结点
2.3 MyLinkedList类的成员方法
2.3.1 在链表开头插入一个新结点
在链表开头插入一个结点,首先需要根据 data 数据实例化一个结点。然后让这个新结点的 next 指针域存 head 的地址, 这样就让新的结点与后面的结点链接起来了,最后让 head 等于这个新结点,这样这个新结点就变成了第一个结点
//头插法
public void addFirst(int data) {
Node nodeTmp = new Node(data);
nodeTmp.next = this.head;
this.head = nodeTmp;
}
2.3.2 获取链表的最后一个结点
首先 head 不能移动,如果移动第一个结点就会因没有被指向而回收,那么后面结点也就会因为没有被指向而被回收,所以需要创建一个临时的结点类型的变量 src,将 head 指向的结点赋值给它,这样 head 和 src 同时指向了第一个结点,当src 移动到其他结点,第一个结点还是会被 head 所指向。src 通过循环一直等于下一个结点,当 src 存储的结点的 next 为 null 时就为最后一个结点
//获取链表的最后一个结点
private Node lastNode() {
Node src = this.head;
while (src.next != null) {
src = src.next;
}
return src;
}
注:这个方法是private修饰所以只能在 MyLinkedList类 中使用
2.3.3 在链表尾部插入一个新的结点
在链表尾部插入一个结点,首先需要这个链表是否为空,如果链表为 null ,尾插一个结点就相当于在链表开头插入一个结点。如果链表不为 null ,尾部插入一个结点就需要根据 data 数据实例化一个结点,然后通 lastNode 方法获取最后一个结点,让最后一个结点的 next 存储新结点的地址即可
//尾插法
public void addLast(int data) {
if (head == null) {
addFirst(data);
return;
}
Node nodeTmp = new Node(data);
Node last = lastNode();
last.next = nodeTmp;
}
2.3.4 返回指定位置的前一个结点
首先判断位置是否合法,如果不合法直接返回 null ,否则用一个临时结点变量等于 head 指向的结点,然后通过循环 index - 1 次而找的指定位置的前一个结点
//返回指定位置的前一个结点
private Node prevNode(int index) {
if (index > size() || index < 0) {
return null;
}
Node src = this.head;
while(index != 1) {
src = src.next;
index--;
}
return src;
}
2.3.5 在链表指定位置插入一个新结点
第一步判断指定位置是否合法,如果不合法直接返回 false。否则进入第二步判断插入的位置是否为第一个结点位置,如果是就直接采用头插法在链表的开头插入一个结点。否则进入第三步判断插入的位置是否为最后一个结点的下一个位置,如果是就直接采用尾插法在链表的尾部插入一个结点。否则就在实例化一个 data 数据结点,调用 preNode 方法获取指定位置的前一个结点,在用一个临时变量去存储前一个结点的后一个结点,让前一个结点的 next 存储这个新的结点地址,再让这个新的结点的 next 存储后一个结点地址即可
//任意位置插入,第一个数据结点为0号下标
public boolean addIndex(int index,int data) {
if (index > size() || index < 0) {
return false;
} else if (index == 0) {
addFirst(data);
} else if (index == size()) {
addLast(data);
} else {
Node nodeTmp = new Node(data);
Node prev = prevNode(index);//指定位置前一个结点
Node tmp = prev.next;//指定位置的结点
prev.next = nodeTmp;
nodeTmp.next = tmp;
}
return true;
}
注:if...else if...else if...else...这种多判断只会执行其中的一个
2.3.6 判断是否需要删除第一个结点
判断数据 key 是否与第一个结点数据域中的数据相同,如果相同则让head等于head.next,这样head就指向了下一个结点,也就删除了第一个结点
//判断第一个结点是否是指定的结点,如果是则删除第一个结点
private boolean judgeFistNode(int key) {
if (key == this.head.data) {
this.head = this.head.next;
return true;
}
return false;
}
2.3.6 删除第一次出现的指定数据的结点
首先判断这个链表是否为 null,否则就判断指定数据的结点是否为链表的第一个结点,否则就通过两个结点变量去找这个数据结点以及它的前一个结点,找到之后让它的前一个结点的 next 存储它后结点的地址然后直接return,因为只删除第一次输出的指定数据的结点。如果遍历完都没有找到这个数据结点则没有这个数据结点
//删除第一次出现关键字为key的节点
public void remove(int key) {
if (this.head == null) {
return;
}
boolean judge = judgeFistNode(key);
if (judge == true) {
return;
}
Node src = this.head;
Node slow = this.head;
while (src != null) {
if (src.data != key) {
slow = src;
src = src.next;
} else {
slow.next = src.next;
return;
}
}
}
2.3.7 删除所有指定数据的结点
首先判断这个链表是否为 null, 否则就通过两个结点变量去找这个数据结点以及它的前一个结点,找到之后让它的前一个结点的 next 存储它后结点的地址,一直遍历完,因为要删除所有指定数据的结点。遍历完后在判断第一个结点是否为空,也就相当于如果第一个结点是要删除的结点,先不管它,先删后面的在删第一个结点
//删除所有值为key的节点
public void removeAllKey(int key) {
if (this.head == null) {
return;
}
Node src = this.head;
Node slow = this.head;
while (src != null) {
if (src.data != key) {
slow = src;
src = src.next;
} else {
slow.next = src.next;
src = src.next;
}
}
judgeFistNode(key);
}
2.3.8 查找链表中是否有指定的数据
直接将这个链表遍历一遍,如果有直接返回 true,否则返回 false
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key) {
Node src = this.head;
while (src != null) {
if (src.data == key) {
return true;
}
src = src.next;
}
return false;
}
2.3.9 获取链表的长度
直接将这个链表遍历一遍用一个整型变量计数即可
//得到单链表的长度
public int size() {
Node src = this.head;
int count = 0;
while (src != null) {
count++;
src = src.next;
}
return count;
}
2.3.10 打印链表
首先判断这个链表是否为 null ,如果为空直接打印 null。否则将这个链表遍历一遍,打印每个结点数据域中的数据
//打印链表
public void display() {
if (this.head == null) {
System.out.print("null");
}
Node src = head;
while (src != null) {
System.out.print(src.data + " ");
src = src.next;
}
System.out.println();
}
2.3.11 清空链表
直接让 head 指向 null,这样第一个结点就没有被指向了,会直接被编译器回收。第一个结点被回收了,第二个结点也就没有被指向了也会被回收,依次如此,直到将整个链表全部回收
//清除链表
public void clear() {
this.head = null;
}