链表概念及单链表的模拟实现
链表的概念及结构
链表是一种 物理存储结构上非连续 存储结构,数据元素之间的 逻辑顺序 是通过 地址 来链接的
- 如图所示:链表就像一节火车,结点就像一个个车厢,由中间的横线链接起来
- 而链表的结构也非常多样
1.单向与双向
2.带头与不带头
3.循环与非循环
排列组合后,共有八种链表结构,但我们着重要掌握的是 无头单向非循环 与 无头双向非循环 两种结构
无头单向非循环链表实现
1) 构建结点
private static class LinkNode {
int val;
LinkNode next;
LinkNode(int val) {
this.val = val;
}
}
// head 指向第一个结点
private LinkNode head;
- 这里解释一下为什么无头链表还要多加一个head结点
这是为了更方便一些去调用这个链表
带头链表里面的“头”,里面的数据是无效的
无头链表的“头”,里面的数据是有效的
这么说可能会有一些绕口,但只需记得无头链表里面的头结点只是概念上的头,只是为了方便我们去调用这个链表。
2) 头插法
- 创建一个结点,将该结点与原头结点单向连接,然后头结点改为该结点。
- 即将新结点node 的 next域指向原头结点head,然后头结点head更改为node
//头插法
public void addFirst(int data) {
LinkNode node = new LinkNode(data);
node.next = head;
head = node;
}
-
图解:
-
若链表为空时,这个代码是否依旧可以运行?
答:可以运行 -
图解:空链表head为空,node 的 next域置为空,node变成头结点
3) 尾插法
- 该单链表没有尾指针,所以需要定义一个 cur结点 去找尾结点
- 找到之后,将尾结点与新结点node 单向连接
- 但是这有一个问题,如果链表为空,head已经为null了,cur.next就会发生空指针异常。所以我们还需要加一个判断条件防止这种情况
//尾插法
public void addLast(int data){
LinkNode node = new LinkNode(data);
// 当链表为空,自行添加一个头结点
if (head == null) {
head = node;
return;
}
LinkNode cur = head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
- 图解:
4) 插入指定位置(第一个数据结点为0号下标)
-
index范围:[0,size)
-
思路:
- 插入之前判断所插入的位置 是否合法。链表不像顺序表,不必注意插入是否需要扩容
- 找到要插入的位置,先将后面的结点与新结点node 单向连接 起来,再将其于前面的结点 单向连接
- 图解:假设插入2下标位置,我们就要 找到 2 的前驱结点 1,即 cur 要走 index-1 步。比如图中:index=2,我们就要找到下标1,所以是2-1=1步
// 找index的前驱结点
private LinkNode findPrev(int index) {
LinkNode cur = head;
while (index - 1 != 0) {
cur = cur.next;
index--;
}
return cur;
}
- 找到之后,将新结点node与后继结点单向连接,再与前驱结点单向连接
//任意位置插入,第一个数据节点为0号下标
public boolean addIndex(int index,int data){
if (index < 0 || index > size()) {
return false;
}
// 如果index=0,直接头插法即可
if (index == 0) {
addFirst(data);
}
// 如果index要插入链表最后,尾插法即可
if (index == size()) {
addLast(data);
}
LinkNode cur = findPrev(index);
LinkNode node = new LinkNode(data);
// 插入操作
node.next = cur.next;
cur.next = node;
return true;
}
- 图解:
5) 查找关键字key是否在单链表当中
- 思路:
- 定义一个临时结点cur 用于遍历链表,寻找key
// 查找是否包含关键字key是否在单链表当中
// 定义一个临时结点cur 用于遍历链表,寻找key
public boolean contains(int key){
LinkNode cur = head;
while (cur != null) {
if (cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
- 关于关于cur 与 cur.next != null 的解释
- cur != null 是指 cur 遍历了整个链表,最后cur 指向 null
- 而cur.next != null 是指 cur 只是遍历了链表size-1个结点,最后cur 指向的是最后一个结点
6) 删除第一次出现关键字为key的结点
- 思路:
- 删除操作,直接将删除结点的前驱连接到它的后继即可
- 用临时结点 cur 去遍历链表寻找key
public void remove(int key){
LinkNode cur = head;
while (cur.next != null) {
// cur.next为删除结点
if (cur.next.val == key) {
// cur为删除结点的前驱
cur.next = cur.next.next;
return; }
cur = cur.next;
}
System.out.println("没有你要删除的数据!");
}
- 但是上述代码是用 cur.next 去寻找关键字key,这就意味着头结点是没有找的,所以还需要再加一个判断条件,用于删除头结点。
//删除第一次出现关键字为key的节点
public void remove(int key){
if (head == null) {
// 一个结点都没有,无法删除!
return;
}
if (head.val == key) {
// 将头结点更改为第二个结点
head = head.next;
return;
}
LinkNode cur = head;
while (cur.next != null) {
// cur.next为删除结点
if (cur.next.val == key) {
// cur为删除结点的前驱
cur.next = cur.next.next;
return; }
cur = cur.next;
}
System.out.println("没有你要删除的数据!");
}
- 图解:
7) 删除所有值为key的节点
-
思路:双指针
-
定义两个指针,cur 为要删除的结点,prev 为 cur 的前驱结点
-
cur 用于去寻找要删除的结点(称为危险结点),prev 用于确保 prev指向的结点是不需要删除的结点(称为安全结点)
-
比如 prev 是线头,cur 是剪刀,cur 剪掉不需要的线,剪一个线 线头连一个。正常情况下不可能所有线都要剪掉,所以 cur 会遇到不需要剪掉的线,此时再将这个 安全线 定义为新的线头。重复循环,直到危险结点全部剪掉为止
-
但是这种思路,我们是从第二个结点开始找的,所以头结点就漏掉了。我们需要最后再判断一下头结点是否为key
//删除所有值为key的节点
public void removeAllKey(int key){
LinkNode prev = head;
LinkNode cur = head.next;
while (cur != null) {
if (cur.val == key) {
prev.next = cur.next;
cur = cur.next;
}else {
prev = cur;
cur = cur.next;
}
// cur = cur.next
// 因为if-else语句里面都有cur = cur.next,所以可以精简一下。这
// 里为了好理解,就不精简了
}
if (head.val == key) {
head = head.next;
}
}
- 图解:
8) 得到单链表的长度
- 思路:
1.用临时结点遍历整个链表,再用一个计数器进行计数,返回计数器即可
2.定义一个usedSize用于记录结点个数,直接返回usedSize即可
现使用第一个思路:
//得到单链表的长度
public int size(){
LinkNode cur = head;
int size = 0;
while (cur != null) {
size++;
cur = cur.next;
}
return size;
}
9) 打印单链表
- 遍历链表依次打印
//打印单链表
public void display(){
LinkNode cur = head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
10) 清空列表
- 遍历链表,将每一个结点置空
- 光有一个 cur 去遍历,cur结点置空后,就拿不到后继结点,所以需要加上一个curNext 结点用于定位后继结点
public void clear(){
LinkNode cur = head;
while (cur != null) {
LinkNode curNext = cur.next;
cur.next = null;
cur = curNext;
}
head = null;
}
``