文章目录
链表
概念及结构
链表是一种物理存储结构上非连续存储结构,数据元素的 逻辑顺序 是通过链表中的 引用链接 次序实现的 。
如下图,每一个方框代表一个结点,每一个结点分为数据域与引用域,每个结点的数据域存储数据,引用域则存放下一个结点的引用,各个结点通过引用连接起来,通过下图也可以观察到:每个结点的地址一般是跳跃式的,不一定是连续的,这也是链表与顺序表的区别之一。
链表的种类多种多样,分为带头或不带头、单向或双向、循环或不循环,组合起来共有8种:
- 带头链表,即会创建一个额外的结点,这个结点的引用域引用链表实际的第一个结点,而数据域无意义,通过头结点就可以访问到整个链表
- 双向链表,即每个结点会存在两个引用域,一个引用下一个结点,一个引用上一个结点,对于第一个结点,它没有上一个结点(不带头)所以为
null
- 循环链表,最后一个结点的引用域不为
null
,而是引用第一个结点,如此达到循环的效果
虽然链表的结构有8种,但是我们重点掌握两种结构:
- 不带头单向不循环链表: 简称单链表,结构简单,一般不会单独存放数据,一般作为其他数据结构的子结构,如哈希桶、图的邻接表等。但实现操作的代码较困难复杂,许多的题目都是围绕这种结构
- 不带头双向链表: Java集合框架的
LinkedList
的底层就是一个双向链表
单链表的实现
实现一个单链表,并实现基本的操作。首先,我们得有结点,即结点对象,实现一个结点类。
如下代码,外部类为MySingleList
,将结点类ListNode
作为静态内部类,其包含两个成员变量,分别是数据域val
和引用域next
(它将引用下一个结点对象),并给出构造方法。其次,为了方便操作,我们再定义一个成员变量head
,存放链表的第一个结点的地址。
public class MySingleList implements IList {
//结点内部类
static class ListNode {
public int val;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
//链表的第一个结点
public ListNode head;
//重写的方法
}
实现了我们自定义的IList
接口,所以必须重写方法,数据结构一定要多画图!建议结合下面的代码好好画图!
public interface IList {
//头插法
public void addFirst(int data);
//尾插法
public void addLast(int data);
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data);
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key);
//删除第一次出现关键字为key的节点
public void remove(int key);
//删除所有值为key的节点
public void removeAllKey(int key);
//得到单链表的长度
public int size();
//清空
public void clear();
//打印
public void display();
}
public void display()
先从最简单的打印入手,假设我们已经有一个链表,并且已经有第一个结点的引用head
思路:利用结点的引用域不断向后遍历链表并打印数据
//打印链表元素
@Override
public void display() {
//临时变量,避免修改成员变量head
ListNode cur = this.head;
//为null停下
while(cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;//向后寻找
}
System.out.println();//手动换行
}
public int size()
思路:定义一个计数器,遍历链表,不断计数
//链表结点数
@Override
public int size() {
int count = 0;
ListNode cur = this.head;
while(cur != null) {
count++;
cur = cur.next;
}
return count;
}
public void clear()
清空有"暴力"清空和非暴力清空,"暴力清空"只需要head == null;
即可,这里我们给出非暴力清空,即将每个结点的next
均置为null
并将head
置为null
//清空
@Override
public void clear() {
ListNode cur = head;
while (cur != null) {
ListNode curN = cur.next;
cur.next = null;
cur = curN;
}
head = null;
}
public void addFirst(int data)
头插即将新的结点插入到链表的头部,使其成为链表新的头部
- 必须申请一个新的结点对象
- 考虑链表为空的情况,即
head == null
//头插
@Override
public void addFirst(int data) {
//实例化一个结点对象
ListNode newNode = new ListNode(data);
//链表为空
if(this.head == null) {
this.head = newNode;
return;
}
//链表不为空
newNode.next = head;
head = newNode;
}
public void addLast(int data)
尾插即将新结点插入到链表结尾,使其成为新的尾结点
- 考虑链表为空的情况
- 想要尾插,必须找到最后一个结点,让它的
next
指向新的结点,按照之前的循环条件cur != null
,cur
会走到null
,所以我们改变循环条件为cur.next != null
,这样就能保证cur
最后指向尾结点
如此我们有:
- 如果想遍历链表的每个结点,循环条件为
cur != null
- 如果想找到尾结点,循环条件为
cur.next != null
//尾插
@Override
public void addLast(int data) {
//实例化一个结点对象
ListNode newNode = new ListNode(data);
//链表为空
if(this.head == null) {
this.head = newNode;
return;
}
//链表不为空
ListNode cur = this.head;
while(cur.next != null) {
cur = cur.next;
}
cur.next = newNode;
}
public void addIndex(int index,int data)
任意位置插入,前提:假设第一个结点的位置为0,以此类推,在指定位置插入新结点。
思路:必须找到原链表指定位置的前一个结点,让它的next
指向新结点,并将新结点的next
指向原结点。
-
这里我们自定义了一个异常,“下标非法异常”
public class IllegalIndexException extends RuntimeException { public IllegalIndexException(String message) { super(message); } }
- 我们采用了让
cur
循环结束时停留在指定位置结点的前一个结点的位置,这样我们就可以拿到所需的所有结点,注意这里的语句顺序,必须先让新结点的next
指向原来的指定位置结点,然后再让原指定位置结点的前一个结点指向新结点。
- 我们采用了让
//指定位置插入,假设第一个结点标记为0位置
@Override
public void addIndex(int index, int data) {
//非法下标,抛出自定义异常
if(index < 0 || index > size()) {
throw new IllegalIndexException("Illegal Index !:下标非法");
}
//插入位置为0,相当于头插
if(index == 0) {
addFirst(data);
return;
}
//寻找指定下标的前一个结点,方便更改链表的指向
ListNode newNode = new ListNode(data);
ListNode cur = this.head;
for(int i = 0; i < index - 1; i++) {
cur = cur.next;
}
//注意语句顺序
newNode.next = cur.next;
cur.next = newNode;
}
public boolean contains(int key)
- 链表为空,肯定不包含任何结点
- 不为空,遍历寻找即可
//检查是否包含某个元素
@Override
public boolean contains(int key) {
if(this.head == null) {
return false;
}
ListNode cur = this.head;
while(cur != null) {
if(cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
public void remove(int key)
删除结点,要让它的前一个结点的next
指向它的后一个结点
- 自定义了一个异常:“链表为空的异常”,空链表无法删除(看个人喜好)
- 情况一:第一个结点就为待删除结点
- 情况二:情况一之外,代码会直接从第二个结点开始判断,所以我们事先判断是否为情况一
//删除第一个指定的数据
@Override
public void remove(int key) {
if(this.head == null) {
throw new ListIsEmptyException("The SingleList is Empty!: 链表为空!");
}
//第一个结点满足
if(head.val == key) {
head = head.next;
return;
}
//遍历寻找并删除
ListNode cur = this.head;
while(cur.next != null) {
if(cur.next.val == key) {
cur.next = cur.next.next;
break;
}
cur = cur.next;
}
}
public void removeAllKey(int key)
这个方法考虑的事情很多,代码一开始判断是否为空链表,然后跳过第一个结点向后寻找待删除的结点,方法是:定义两个引用,一个是寻找引用cur
,它负责向后寻找待删除结点,另一个prev
是为了执行删除操作,改变它的指向。
-
当
cur
指向的结点不满足条件,则两个引用均向后一位(注意语句顺序) -
当
cur
指向的结点满足条件,则prev
不动,让prev
指向cur
的下一个结点,删除成功,然后cur
向后一位。为什么
prev
不动呢? 为了解决多个待删除的结点连续的情况
最后,判断第一个结点是否为待删除结点,执行操作。
//删除全部的指定数据
@Override
public void removeAllKey(int key) {
if(this.head == null) {
throw new ListIsEmptyException("The SingleList is Empty!: 链表为空!");
}
//先删除所有第一个结点后面的指定结点
ListNode prev = this.head;
ListNode cur = prev.next;
while(cur != null) {
if(cur.val == key) {
prev.next = cur.next;
}else {
prev = cur;
}
cur = cur.next;
}
//最后检查第一个结点是否满足删除条件
if(this.head.val == key) {
head = head.next;
}
}
完整实现如下(接口和异常类不额外再给出了):
public class MySingleList implements IList {
//结点内部类
static class ListNode {
public int val;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
//