🚆上一篇文章绍顺序表的基本使用,这篇文章将介绍单向链表的一些基础操作及实现。
目录
一、链表
🍓 链表的概念及结构
- 链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的,链表是由一个一个的结点(数据域、指针域)组成。
✨链表的结构非常多样,以下情况组合起来就有8种链表结构:
- 🍎单向、带头、循环
- 🍎单向、带头、非循环
- 🍎单向、不带头、循环
- 🍎单向、不带头、非循环
- 🍎双向、带头、循环
- 🍎双向、带头、非循环
- 🍎双向、不带头、循环
- 🍎双向、不带头、非循环
🌌虽然有这么多的链表的结构,但是重点掌握的有两种:
- ⭐不带头、单向、非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
- ⭐不带头、双向、非循环链表:在Java的集合框架库中LinkedList底层实现就是不带头、双向、非循环链表。
✨区分单向单头非循环链表与单向不带头非循环链表
二、单向不头非循环链表
- 定义一个 MyLinkedList 类实现链表的基本操作
- 因为一个结点是由数据域和地址域共同组成,所以定义一个ListNode类生成结点
- 定义一个 Test 类调用 MyLinkedList 类中所实现的方法进行测试。
//ListNode 代表一个结点
class ListNode{
public int value;
//next存放下一个结点的地址,所以定义它的类型为ListNode(结点类型)
public ListNode next;
//定义一个带一个参数的构造方法,对链表赋值
public ListNode(int value){
this.value = value;
}
}
public class MyList {
//head 属于链表的属性,它引用的是链表的头
//指代MyList这个链表的头引用(指向第一个结点)所以定义在链表中
public ListNode head; //链表的头引用
public static void main(String[] args) {
ListNode listNode1 = new ListNode(12);
}
}
使用穷举法创建一个链表感受一下链表是怎样生成的(这个方法很low,只是为了演示),真正的创建链表是使用头插法与尾插法。
🌊在 MyLinkedList 类中写一个 createList 方法创建链表。
public class MyList {
public ListNode head; //链表的头引用
public void createList(){
ListNode listNode1 = new ListNode(12);
ListNode listNode2 = new ListNode(23);
ListNode listNode3 = new ListNode(34);
ListNode listNode4 = new ListNode(45);
ListNode listNode5 = new ListNode(56);
}
创建结点后,可以通过第一个结点的next保存第二个结点的地址,以此类推,到最后一个结点listNode5 不用进行任何操作(next域默认为null),这样就可以将每个结点串起来。再将head指向listNode1(第一个结点)。
public class MyList {
public ListNode head; //链表的头引用
public void createList(){
ListNode listNode1 = new ListNode(12);
ListNode listNode2 = new ListNode(23);
ListNode listNode3 = new ListNode(34);
ListNode listNode4 = new ListNode(45);
ListNode listNode5 = new ListNode(56);
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4;
listNode4.next = listNode5;
this.head = listNode1;
}
}
- 当执行完这个方法后,listNode1 ~ listNdoe5 就会被回收(局部变量),而head是成员变量不会被回收,这样就创建出了一个链表。
🍓打印链表
- 定义一个 display 方法打印链表的数据。
因为head指向链表中的第一个结点,即可以通过head遍历链表,但是当遍历完最后一个结点时head指向的地址为null(head的值为null),如果head的值为null,那么就会找不到链表的头结点,找不到头结点就找不到整个链表,所以定义一个ListNode类型的变量cur指向head,使用成员变量 cur 遍历链表cur = cur.next。
public void display(){
ListNode cur = this.head; //定义一个变量cur指向head
//结点中next域的值为null 代表是最后一个结点
while (cur!=null){
System.out.print(cur.value+" ");
//将写一个结点的地址赋给cur,cur就会走到下一个结点
cur = cur.next;
}
System.out.println();
}
🌊链表的创建与打印都实现了,接下来定义一个 Test 类调用这两个方法,测试一下
🛫了解了链表的基本结构,接下来就可以实现链表的基本方法。
三、单向不带头非循环链表方法的实现
🍓判断关键字key是否在链表中
- 遍历整个链表,如果有数据与key相等就表示找到了,返回true,否则返回false。
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key){
ListNode cur = this.head;
while (cur!=null){
if(cur.value==key){
return true;
}
cur = cur.next;
}
return false;
}
🍓获取链表长度
- 定义一个计数器 count,用 cur 遍历整个链表,当cur !=null,count就自增,遍历完整个链表,返回count 的值。
//得到单链表的长度
public int size(){
ListNode cur = this.head;
int count = 0;
while (cur!=null){
count++;
cur = cur.next;
}
return count;
}
🍓头插法
💦前面使用穷举法创建链表只是为了接收链表的结构,链表正确的创建是使用头插法和尾插法。
🍉顺序表出入数据的时候要判断是否满了,链表插入数据不用判断,直接插入就可以了。
- 链表是随用随取,要插入一个数据,就创建一个结点。
- 头插法是在第一个结点前面插入结点,创建一个结点将数据传参(ListNode类中定义了一个有参的构造方法)。
- 插入有两种情况,一种是在有结点的链表中进行插入,另一种是在没有结点的链表(空链表)中插入。
//头插法
public void addFirst(int data){
ListNode node = new ListNode(data);
node.next = this.head;
this.head = node;
//分开的写法
/* if(this.head==null){
this.head = node;
}else {
node.next = this.head;
this.head = node;
}*/
}
🍓尾插法
- 尾插法是在最后一个结点的后面插入结点。
- 遍历整个链表,如果某一个结点的next是null,就表示它是最后一个结点,然后在该结点的后面插入结点。
- 插入有两种情况,一种是在有结点的链表中进行插入,另一种是在没有结点的链表(空链表)中插入。
//尾插法
public void addLast(int data){
ListNode node = new ListNode(data);
if(this.head==null){
this.head = node;
}else {
ListNode cur = this.head;
while (cur.next!=null){
cur = cur.next;
}
//循环结束后 cur.next = null
cur.next = node;
}
}
🍓在任意位置插入结点,第一个节点的下标为0
- 在链表的中间位置插入结点,首先要找到待插入位置的前一个结点,假设要插入的位置为index,就要找到index-1位置的结点。
- 写一个 findIndex 方法查找 index-1位置(因为返回的是地址,所以方法的返回类型是ListNode)。
- 写一个方法插入节点,判断传入的参数,如果index<0 或者大于链表的长度(可以直接调用前面所写的返回链表长度的方法)位置不合法。
- 如果插入的位置为0,就相当于头插,直接调用头插法。
- 如果插入的位置是最后一个结点的后面(因为是从0位置开始,最后一个结点后面的位置就是等于链表的长度),就相当于尾插,就直接调用尾插法。
//找到index-1的位置
public ListNode findIndex(int index){
ListNode cur = this.head;
while (index-1!=0){
cur = cur.next;
index--;
}
return cur;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
if(index<0 || index >size()){
System.out.println("index位置不合法!");
return;
}
if(index ==0 ){
addFirst(data);
return;
}
if(index == size()){
addLast(data);
return;
}
ListNode node = new ListNode(data);
ListNode cur = findIndex(index);
node.next = cur.next;
cur.next = node;
}
🍓删除第一次出现关键字为key的节点
- 先判断链表是否为空,如果为空就直接返回。
- 判断链表中结点的值是否与待删除结点的值相同。
- 如果删除的是头结点,让head指向下一个结点。head = head.next;
- 写一个 findPreNode 方法判断待删结点的前驱,如果找到了就返回该结点的引用,找不到就表示链表中没有要删除的结点,返回null。
- 调用 findPreNode 方法,该方法返回待删除的前驱结点,通过前驱结点定义待删除的结点的引用为del。删除结点:cur.next = del.next;(因为del是一个局部变量,出来该方法del指向的空间就会被回收,所以删除结点后不用将其置为null)
- 如果 findPreNode 方法返回null,就表示链表中没有要删除的结点。
//找待删除结点的前驱
public ListNode findPreNode(int key){
ListNode cur = this.head;
while (cur.next!=null){
if(cur.next.value==key){
return cur;
}
cur =cur.next;
}
return null;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
if(this.head==null){
System.out.println("链表为空");
return;
}
if(this.head.value == key){
this.head = this.head.next;
return;
}
ListNode cur = findPreNode(key);
if(cur==null){
System.out.println("没有要删除的结点");
return;
}
ListNode del = cur.next;
cur.next = del.next;
}
🍓删除所有值为key的节点
- 先判断链表是否为空,如果为空就直接返回。
- 要删除所有值为key的结点有两种情况key值是不连续的与连续的。
- 头结点最后判断是否要删除,如果一开始就判断是否要删除头结点,那么prev与cur就不好赋值。
🍉key值是不连续:
✨key值是连续:
//删除所有值为key的节点
public void removeAllKey(int key){
if(this.head==null){
System.out.println("链表为空");
return;
}
ListNode prev = this.head;
ListNode cur = this.head.next;
while (cur!=null){
if(cur.value==key){
prev.next = cur.next;
cur = cur.next;
}else {
prev = cur;
cur = cur.next;
}
//最后处理头结点
if(this.head.value==key){
this.head = this.head.next;
}
}
}
🍓清空链表
- 第一种暴力的方法,直接将头结点置为空,head = null。
- 第二种温柔的方法,使用head遍历每一个结点,将每一个结点置为空。
public void clear(){
//this.head = null; //暴力的方法
//温柔的方法
while (this.head!=null){
ListNode curNext = this.head.next;
this.head = null;
this.head = curNext;
}
}