🚈1. 单链表的概念
首先看一下什么是链表?
单向链表就像一个铁链一样,一个链接着一个,环环相扣。
-
链表是一种在物理上非连续、非顺序的数据结构,由若干节点所组成。
-
单向链表的每一个节点包含两部分,一部分是存放数据的变量
val
,另一部分是指向下一个节点的指针next
。 -
链表的第1个节点被称为头节点,最后1个节点被称为尾节点,尾节点的
next
指针指向空。 -
与数组按照下标来随机寻找元素不同,对于链表中的一个节点,我们只能通过头节点(A)的next指针找到该节点的下一个节点(B),再根据节点B的next指针找到下一个节点C……
- 单链表有且只有一个后继,可以有多个前驱
图一:
图二:
解析:
🌕 图一满足单链表要求,因为链表是要求环环相扣的,核心是一个节点只能有一个后继,但不代表一个节点只能被一个节点指向(可以有多个前驱)。例如:节点c1只有一个后继c2,但有两个前驱a2和b3。
🌕 图二就不满足要求,因为c1有两个后继a5和b4。
注意:
在做题的时候要注意比较的是值还是节点,有时两个节点的值相等,但不是同一个节点。如下图,节点 8 的两个前驱的值相等(都为1),但不是同一个节点。
🚈2. 链表的定义方式
单链表,一般知道了第一个节点(头节点),就可以通过遍历的方式找到所有节点,所以第一个节点很重要。
链表定义
链表的节点,由数据域和指针域组成
指针域保存的是下一个节点的地址,即指向下一个节点
// 单链表
public static class ListNode {
// 创建节点
int val; // 数据域 当前节点的值
ListNode next; // 指针域 指向下一个节点
// 构造方法
public ListNode(int val) {
this.val = val;
next = null; // 可以省略
}
}
🚈3. 链表的基本实现
- 链表节点的创建
- 链表长度
- 打印链表
- 插入
- 删除
代码整体部分,详细解释请看下文😎
MySingleList 类(主体类):
public class MySingleList {
// 单链表
public static class ListNode {
// 创建节点
int val; // 数据域
ListNode next; // 指针域
// 构造方法
public ListNode(int val) {
this.val = val;
next = null; // 可以省略
}
}
ListNode head;
/**
* 计算链表长度
* @return
*/
public int size() {
ListNode cur = head;
int count = 0;
while (cur != null) {
count++;
cur = cur.next;
}
return count;
}
// 链表打印
public void display() {
ListNode cur = head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
//查找是否包含关键字key在链表中
public boolean contains(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
/**
* 链表插入,
* 头部插入、尾部插入、中间插入
*/
public void addFirst(int val) {
// 特殊情况,如果head 为 null
ListNode node = new ListNode(val);
node.next = head;
head = node;
}
// 尾插
public void addLast(int val) {
ListNode node = new ListNode(val);
if (head == null) {
head = node;
return;
}
// 找到最后一个结点
ListNode cur = head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
public void addIndex(int index, int val) {
// 判断下标的合法性
checkPositionIndex(index);
// 特殊条件放在前面
if (index == size()) {
addLast(val);
return;
} else if (index == 0) {
addFirst(val);
return;
}
// 找到下标的前一个元素
ListNode preNode = findPreIndexNode(index);
ListNode node = new ListNode(val);
// 插入,先链接后面的节点
node.next = preNode.next;
preNode.next = node;
}
private ListNode findPreIndexNode (int index) {
ListNode cur = head;
while (index-1 != 0) {
cur = cur.next;
index--;
}
return cur;
}
private void checkPositionIndex(int index) {
if (index < 0 || index > size()) {
throw new IndexOutOfBoundsException("index非法 " + " Index: "+index+", Size: "+size());
}
}
// 删除中间节点
public ListNode removeIndex (int index) {
if (head == null) {
return null;
}
checkElementIndex(index);
if (index == 0) {
head = head.next;
return head;
}
// 找到要删除位置的前一个节点
ListNode preNode = findPreIndexNode(index);
preNode.next = preNode.next.next;
return head;
}
private void checkElementIndex(int index) {
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException("index非法 " + " Index: "+index+", Size: "+size());
}
}
//删除第一次出现关键字为key的节点
public void remove(int key) {
if (head == null) {
return;
}
// 是否包含key
if (head.val == key) {
head = head.next;
}
// 找到key节点的前一个节点
ListNode preNode = findKeyPreNode(key);
// 没有找到
if (preNode == null) {
return;
}
preNode.next = preNode.next.next;
}
private ListNode findKeyPreNode(int key) {
ListNode cur = head;
while (cur.next != null) {
if (cur.next.val == key) {
return cur;
}
cur = cur.next;
}
return null;
}
}
异常处理类:
public class IndexOutOfBoundsException extends RuntimeException{
public IndexOutOfBoundsException() {
super();
}
public IndexOutOfBoundsException(String message) {
super(message);
}
}
Main方法:
可以根据自己的需求进行编写调试~
public static void main(String[] args) {
MySingleList mySingleList = new MySingleList();
MySingleList.ListNode head = mySingleList.head;
mySingleList.addLast(5);
mySingleList.addLast(6);
mySingleList.addIndex(0, 0);
// mySingleList.addIndex(9, 99);
mySingleList.display();
System.out.println(mySingleList.size());
/*System.out.println("============");
mySingleList.removeIndex(1);
mySingleList.display();
System.out.println(mySingleList.size());*/
}
🚦3.1 链表遍历
对于单链表,一定是从头节点开始逐个向后访问,所以操作之后是否还能找到头节点非常重要。切忌把头指针丢了!!!🧐
为了解决“狗熊掰棒子”问题,我们一般另外申请一个变量 cur
,使其等于头指针head
,后序操作就移动cur
就好了~
ListNode cur = head;
// 计算链表的长度
public int size(ListNode head) {
// cur 初始时指向头节点 head
ListNode cur = head;
int count = 0;
// 当链表没有遍历完
while (cur != null) {
count++;
// cur 向后移动一个节点
cur = cur.next;
}
return count;
}
🚦3.2 链表插入
- 头插
头插很简单,只需要将新的节点指向表头就行,newNode.next = head;
别忘了head需要重新指向表头!!! head = newNode;
public void addFirst(ListNode head, int val) {
// 特殊情况,如果head 为 null
// 创建节点
ListNode node = new ListNode(val);
// 将新的节点指向表头
node.next = head;
// head指向新的表头
head = node;
}
- 尾插
只需要找到链表的尾部,然后将其指向新的节点就ok了~
public void addLast(int val) {
// 创建新的节点
ListNode node = new ListNode(val);
// 如果 head = null 说明要插入的节点就是链表的头节点
if (head == null) {
head = node;
return;
}
// 找到最后一个结点
ListNode cur = head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
- 中间插入
这里主要介绍中间插入~
注意:index
下标是从0开始
- 先找到需要插入位置下标的前一个节点,记为
preNode
- 插入新的节点
node
,链接到preNode
的后一个节点 - 将
preNode
节点指向新的节点node
🤔思考:为什么先让待插入节点指向后面的节点?
难点是如何找到要插入的节点的前一个节点?
public void addIndex(int index, int val) {
// 判断下标的合法性
checkPositionIndex(index);
// 特殊条件放在前面
if (index == size()) {
addLast(val);
return;
} else if (index == 0) {
addFirst(val);
return;
}
// 找到下标的前一个元素
ListNode preNode = findPreIndexNode(index);
ListNode node = new ListNode(val);
// 插入,先链接后面的节点
node.next = preNode.next;
preNode.next = node;
}
// 找到下标的前一个元素
private ListNode findPreIndexNode (int index) {
ListNode cur = head;
// 只需要让cur向后移动index - 1步
while (index-1 != 0) {
cur = cur.next;
index--;
}
return cur;
}
🚦3.3 链表删除
删除同样分为头部删除、中间删除和尾部删除。
🌋注意:当链表本身就为空时,不能删除!
- 头删
头删只需要执行 head = head.next
即可。将head
向前移动一次
- 尾删
找到删除的节点的前驱节点cur
,将其的next
指向空 cur.next = null
即可
- 中间删除
只需要找到要删除节点的前一个节点preNode
, 找到后,将preNode.next
的指针的值更新为preNode.next.next
就可以解决了~
// 删除中间节点
// index 从0开始
public ListNode removeIndex (int index) {
// 如果链表为空直接返回
if (head == null) {
return null;
}
// 检查下标的合法性
checkElementIndex(index);
// 如果下标为0,头删
if (index == 0) {
head = head.next;
return head;
}
// 找到要删除位置的前一个节点
ListNode preNode = findPreIndexNode(index);
preNode.next = preNode.next.next;
return head;
}
private void checkElementIndex(int index) {
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException("index非法 " + " Index: "+index+", Size: "+size());
}
}
private ListNode findPreIndexNode (int index) {
ListNode cur = head;
while (index-1 != 0) {
cur = cur.next;
index--;
}
return cur;
}
🏝️补充
我们在刷LeetCode的时候,主体函数部分是不需要编写的,只需要写具体函数实现就好了,但是这个对于新手来说很不友好,调试很不方便,下面给一个参考~~
public class Main {
/**
* 初始化链表
* @param array
* @return
*/
private static ListNode initLinkedList (int[] array) {
ListNode head = null, cur = null;
for (int i = 0; i < array.length; i++) {
ListNode newNode = new ListNode(array[i]);
newNode.next = null;
if (i == 0) {
head = newNode;
cur = head;
} else {
cur.next = newNode;
cur = newNode;
}
}
return head;
}
/**
* 将链表的值显示出来
* @param head
* @return
*/
public static String toString(ListNode head) {
ListNode cur = head;
StringBuffer stringBuffer = new StringBuffer();
while (cur != null) {
stringBuffer.append(cur.val).append(" ");
cur = cur.next;
}
return stringBuffer.toString();
}
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4};
ListNode head = initLinkedList(arr);
/**
* 这部分是需要调用的函数
*/
System.out.println(toString(head));
}
}