文章目录
在我们实现过顺序表后,不难发现顺序表在实现增加删除时要进行 整体的前搬或后移,对于任意位置的插入十分不方便,这里我们就需要链表来进行更好的实现。
一、对于链表的基本解释
抽象形式:
概念:
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。
注:
1.链式结构在逻辑上是连续的,但是在物理上不是连续的。
2.实现中结点一般是在堆上申请出来的。
3.堆上申请出来的结点可能连续,也可能不连续。
链表的结构可以分为3大块:
1.单向或双向。
2.带头或不带头。
3.循环或非循环。
排列组合下来可以有8种不同的组合,但是相对简单且基础的是,无头结点的单向链表。
二、无头结点的单向链表的实现
1.基本框架的构建
在进行各项方法实现之前,我们最先需要了解的是结点的形式,如图:
不难看出,每一个结点都是一个独立的空间,那么,在实现结点的程序时,就可以将其定义为一个内部类开辟申请空间,代码如下:
static class ListNode{
//实现数据域
public int val;
//实现指针域
public ListNode next;
//实现数据域的构造方法
public ListNode (int val){
this.val = val;
}
}
//实现一个头结点用来指向链表的第一个元素
public ListNode head;
接下来,我们要了解链表需要实现哪些方法:
1.头插法
2.尾插法
3.插入任意位置
4.查找是否包含某些数据在链表中
5.删除指定的第一个出现的元素
6.删除链表中指定出现的全部元素
为了我们更好的实现不同的方法,我们可以最先实现打印方法,便于我们更加直观实现方法。代码如下:
public void display(){
//定义一个car来作为遍历的指针
ListNode car = this.head;
while(car != null){
System.out.print(car.val+ " ");
car = car.next;
}
System.out.println();
}
2.头插法
逻辑解释:
首先,假设已经有了两个结点,如图:
头插,顾名思义就是要将元素放到链表的头部,再将head指针指向新的元素。
操作如图:
步骤1 :首先要将头结点指向的元素地址传递给新的节点
步骤2:再将 head 指针指向新的 node 元素。
代码如下:
//头插法
public void addFirst(int data){
ListNode node = new ListNode(data);
//将头结点指向的元素的地址传给新结点的next
node.next = head;
//将新结点的地址传给head
head = node;
}
代码测试:
public class Test {
public static void main(String[] args) {
SignalLinkedList signalLinkedList = new SignalLinkedList();
signalLinkedList.addFirst(1);
signalLinkedList.addFirst(2);
signalLinkedList.addFirst(3);
signalLinkedList.addFirst(4);
signalLinkedList.display();
}
}
不难理解,因为是头插法,数字在打印出来时顺序正好相反。
3.尾插法
逻辑实现:
同理假设有两个结点,如图:
这里插入的逻辑思考并不难,直接将链表最后一个元素的 next 存储新结点node 的地址即可,但是,这里我们就需要考虑如何找到链表的最后一个元素,我们可以定义一个 car 专门来寻找它。
操作如图:
步骤1:找到链表最后一个元素
步骤2:将新结点的地址传递给最后一个元素。
代码如下:
public void addLast(int data){
ListNode node = new ListNode(data);
ListNode car = this.head;
//这里要判断是否为第一个元素
if(car == null){
this.head = node;
}
else{
while(car.next != null){
car = car.next;
}
car.next = node;
}
}
代码测试:
public class Test {
public static void main(String[] args) {
SignalLinkedList signalLinkedList = new SignalLinkedList();
signalLinkedList.addLast(1);
signalLinkedList.addLast(2);
signalLinkedList.addLast(3);
signalLinkedList.addLast(4);
signalLinkedList.display();
}
}
4.任意位置插入
在实现这个操作之前,我们需要先实现一个可以确定链表长度的方法,这个在之后会用到。
代码如下:
public int size(){
int count = 0;
ListNode car = this,head;
while(car != null){
count++;
car = car.next;
}
return count;
}
逻辑解释:
首先,假设已经有了一串链表,如图:
这里插入就有很多要注意的地方,插入的位置不能小于链表长度的最小值,不能大于链表的长度。至于,在合适位置插入的方法,逻辑不难理解,要知道,先接后,在连前。
操作如图:
步骤1:定义一个 car 指针找到要插入位置的上一个位置。
步骤2:将 car 指向的后继结点的地址传递给 node。
步骤3:将 node 的地址传递给上一个元素。
代码如下:
这里判断合法性还需要一个方法实现:
//合法性判断方法
public class IndexWrongFulException extends RuntimeException {
public IndexWrongFulException() {
}
public IndexWrongFulException(String message) {
super(message);
}
}
public void addIndex(int index,int data){
if(index < 0 || index > size()){
System.out.println("index不合法");
throw new IndexWrongFulException("index不合法");
}
//当index为0时等于头插法
if(index == 0){
addFirst(data);
return;
}
//当index为链表长度时可以用尾插法
if(index == size()){
addLast(data);
return;
}
//这里可以定义一个内部私有方法来确定car的位置
private ListNode findIndexSub(int index){
ListNode car = this.head;
while(index != 0){
car = car.next;
index--;
//随着index-- car的位置在不断向前
}
return car;
}
//实现正常插入操作
ListNode node = new ListNode(data);
ListNode car = findIndexSub(index);
node.next = car.next;
car.next = node;
}
代码测试:
1.测试报错代码
public class Test {
public static void main(String[] args) {
SignalLinkedList signalLinkedList = new SignalLinkedList();
signalLinkedList.addLast(1);
signalLinkedList.addLast(2);
signalLinkedList.addLast(3);
signalLinkedList.addLast(4);
signalLinkedList.addIndex(-1,99);
signalLinkedList.display();
}
}
2.测试正常代码
public class Test {
public static void main(String[] args) {
SignalLinkedList signalLinkedList = new SignalLinkedList();
signalLinkedList.addLast(1);
signalLinkedList.addLast(2);
signalLinkedList.addLast(3);
signalLinkedList.addLast(4);
signalLinkedList.addIndex(2,99);
signalLinkedList.display();
}
}
5.查找 是否 包含某些数据在链表中
逻辑解释:
这里的是否表明这个方法是只要证明是否有该数字,所以为 boolern 类型,只需要定义一个 car 指针在链表中进行扫描即可。
代码实现:
public boolern contains(int key){
ListLnode car = this.head;
while(car != null){
if(car.val == key){
return true;
}
car = car.next;
}
return false;
}
6.删除 指定的第一个 出现的元素
逻辑解释:
同样,首先假设有一串链表。
假设,这里要删除的元素为 2 ,呢么根据要求只要可以删去第一个 2 就可以了,同样,这里还要考虑链表长度为0,链表中没有要删除的元素的情况,除过这些操作,正常删除的逻辑也不难理解,如下图解释:
逻辑1:定义一个car指针找到要删除的值
逻辑2:将要删除的值的 next 存放的下一个元素的地址传递给 car.next
代码实现:
public void remove(int key){
//判断如果为空
if(this.head == null){
return 0;
}
//如果第一个元素就是要删除的数值
if(this.head == key){
this.head = this.head.next;
return;
}
//这里定义一个内部方法用来实现对要删除元素位置的查找
private LinkNode findRomve(int key){
LinkNode car = this.head;
while(car != null){
if(car.next.val == key){
return car;
}
car = car.next;
}
return null;
}
LinkNode car = findRomve(key);
if(car == null){
System.out.println("没有找到要删除的值");
return;
}
car.next = car.next.next;
}
代码测试:
1.没有找到要删除的值
public class Test {
public static void main(String[] args) {
SignalLinkedList signalLinkedList = new SignalLinkedList();
signalLinkedList.addLast(1);
signalLinkedList.addLast(2);
signalLinkedList.addLast(2);
signalLinkedList.addLast(4);
signalLinkedList.remove(99);
}
}
2.正常删除
public class Test {
public static void main(String[] args) {
SignalLinkedList signalLinkedList = new SignalLinkedList();
signalLinkedList.addLast(1);
signalLinkedList.addLast(2);
signalLinkedList.addLast(2);
signalLinkedList.addLast(4);
signalLinkedList.remove(2);
signalLinkedList.display();
}
}
7.删除链表中 指定出现的 全部元素
逻辑解释:
同样,我们首先有一个链表,如图:
同样假设这里要删除的值为 2 ,这里可以定义两个指针,一前一后,前面的指针进行搜索,后面的指针进行跨越。如图:
逻辑1:定义两个指针确定位置
逻辑2:car 发现是要删除的结点,将下一个结点的地址传递给 prev 指向的 next
逻辑3:car 向后移动
逻辑4:与逻辑 2 相同
逻辑5:与逻辑 3 相同
逻辑6:car 指向的元素不是要删除的元素,将 prev 移动过去。
逻辑7:在判断头结点的值也为要删除的元素后,将 head 指向 prev。
代码实现:
public void removeAllKey(int key){
//首先判断链表是否存在
if(this.head == null){
return;
}
ListNode car = this.head.next;
ListNode prev = this.head;
while(car != null){
if(car.val == key){
//直接跨过要删除的数据
prev.next = car.next;
car = car.next;
}
else{
//出现不需要删除的数据时
prev = car;
car = car.next;
}
}
//除头结点查找完后,检查头结点是否为要删除元素
if(this.head.val == key){
this.head = prev;
}
}
代码测试:
public class Test {
public static void main(String[] args) {
SignalLinkedList signalLinkedList = new SignalLinkedList();
signalLinkedList.addLast(2);
signalLinkedList.addLast(2);
signalLinkedList.addLast(2);
signalLinkedList.addLast(4);
signalLinkedList.removeAllKey(2);
signalLinkedList.display();
}
}
8.总结
到这里链表的实现已经基本完成,并不是很复杂,但是逻辑分析十分重要,最后,在这里实现一个清空链表就圆满完成了!
public void clear(){
this.head = null;
}
感谢您的阅读,如有问题,欢迎指正。