首先看一下什么是链表,单向链表就如同铁链一样,元素之间相互连接,包含多个结点,每个节点有一个指向后继元素的 next 指针。表中最后一个元素的 next 指向 null。如下图:
思考一下
你是否理解了链表的含义,思考一下下面两个图,是否都满足单链表的要求,为什么?
第一个图 :
第二个图:
解析:
上面第一个图是满足单链表的要求的,因为我们说链表要求环环相扣,核心是一个结点只能有一个后继,但是不代表一个结点只能有一个被指向。第一个图中,c1 被 a2 和 b3 同时指向,这是没关系的。这就好比法律倡导一夫一妻的制度,但是你只能爱一个人,但是可以多个人同时爱你。
第二个图就不满足要求了,因为 c1 有两个后继 a5 和 b4。
另外再做题的时候比较要注意的是值还是结点,因为有时候两个结点的值可能相等,但是并不是同一个结点,例如下图中,有两个结点的值都是 1,但并不是同一个结点。
节点和头节点
在链表中,每个点都由值和指向下一个结点的地址组成的独立的单元,称为一个结点,有时候也称为节点,含义都是一样的。
对于单链表,如果知道了第一个元素,就可以通过遍历访问整个链表,因此第一个结点最重要,一般称为头结点。
虚拟节点
在做题以及在工程里面经常可以看到虚拟节点的概念,其实就是一个节点 dummyNode,其 next 指针指向 head,也就是 dummyNode.next=head。
因此,如果我们在算法里面使用了虚拟节点,则要注意如果获得 head 节点,或者从方法(函数)里返回的时候,应该使用 dummyNode.next。
另外注意,dummyNode 的 val 不会被使用,初始化 0 或者 -1 都是可以的,既然不会被使用,那么虚拟节点有什么作用呢?简单来说,就是为了方便我们处理头部节点,否则我们需要在代码里单独处理首部节点的问题,在链表反转里面,我们会看到该方式大大降低解题难度。
我们看如何构造链表。
首先,要理解 JVM 是如何构建出链表的,我们知道 JVM 里面有堆区以及栈区,栈区主要负责引用,也就是一个指向实际对象的地址,而堆区存的才是创建的对象,例如我们顶一个这样的类:
public class Course{
Teacher teacher;
Student student;
}
这里的 teacher 和 student 就是指向堆的引用,假如我们这样定义:
public class Course{
int val;
Course next;
}
这个时候 next 就指向了下一个同为 Course 类型的对象了,例如:
这样通过栈的引用(也就是地址),就可以找到 val (1),然后 val (1)节点又存了指向 val (2)的地址,然后 val (3)又存了指向 val (4)的地址,就这样,构造出了一个链表访问结构。
在 Java 配套代码中 BasicLink 类,我们 debug 一下看一下就会发现下图:
这样就是一个简单的线性访问了,所以链表就是从 head 开始,逐个开始向后访问,然后每次所访问对象的类型都是一样的。
根据面向对象的理论,在 Java 里面规范的链表定义如下:
public class LinkList {
private int data;
private LinkList next;
public LinkList(int data) {
this.data = data;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public LinkList getNext() {
return next;
}
public void setNext(LinkList next) {
this.next = next;
}
}
但是在 LeetCode 中算法题中经常使用以下方式来创建链表:
public class ListNode{
public int val;
private ListNode next;
ListNode (int x){
this.val = x;
this.next = null;
}
}
ListNode listnode= new ListNode(1);
这里的 val 就是当前节点的值,next 指向下一个结点。因为两个变量都是 public 的,创建对象后能直接使用 listnode.val 和 listnode.next 来操作,虽然这样不满足面向对象的设计要求,但是代码更加精简。因此在算法题目中应用更加广泛。
对于单链表,不管进行什么操作,都要从头到后逐个访问,所以操作之后是否还能找到表头非常重要。一定要注意“狗熊掰棒子”问题,也就是只顾当前位置而将标记表头的指针丢掉了。
/**
* 获取链表长度
* @param head 链表的头结点
* @return
*/
public static int getListLength(ListNode head){
int length = 0;
ListNode node = head;
while(node != null){
length++;
node = node.next;
}
return length;
}
/**
* 链表遍历打印
* @param head
*/
public static void printNodeList(ListNode head) {
if(head == null) {
System.out.println("链表为空,无法打印!");
return;
}
int length = 0;
//定义头结点
ListNode node = head;
while (node != null) {
System.out.println(node.data);
length++;
node = node.next;
}
}
单链表的插入和数组的插入一样,分为三种情况,过程并不复杂,但是在编码的时候会发现都是坑。单链表的插入需要考虑三种操作:首部、中部和尾部。
链表表头插入新节点非常简单,容易出错的是可能会经常忘记 head 需要重新指向表头,我们需要创建一个新节点 newNode,怎么连接到原来的链表呢?执行 newNode.next = head 就可以,之后我们要遍历新链表就要从 newNode 开始一路向下了是吧,但是我们海狮习惯用 head 来表示,所以让 head = newNode 就可以了,如下图:
在中间位置插入,首先遍历找到要插入的位置,然后当前位置接入到前驱节点和后继节点之间,但是到了还位置之后我们却不能获得前驱节点,也就无法将节点接入进来。这就好比一边过河一边拆桥,结果自己没办法回去了。
为此,我们要在目标节点前一个位置停下来,也就是使用 cur.next 的值来判断 ,而不是用 cur 的值来判断,这是链表常用的策略。
如下图,如果要在 7 前面插入,当 cur.next = node(7) 的时候就要停下来,此时 cur.val = 15,然后需要给newNode 前后接两根线,此时只能让 new.next = node(15).next(图中虚线),然后 node(15).next = new,而且顺序还不能出错。
为什么不能颠倒顺序呢?
由于每个节点都只有一个 next,因此执行了 node( 15) .next = new 之后,节点 15 和 7之间的连线就自动断开了,如下图所示:
表尾插入就比较容易了,我们只需要将尾部节点插入指向新节点就可以了。
public static ListNode insertNode(ListNode head, ListNode nodeInsert, int position) {
// 头结点为空,直接返回
if (head == null) {
return nodeInsert;
}
// 已经存放的元素格式
int size = getListLength(head);
if (position > size + 1 || position < 1) {
System.out.println("位置参数越界");
return head;
}
//表头插入
if (position == 1) {
nodeInsert.next = head;
//这里可以直接返回 nodeInsert,也可以先将头结点位置前移
head = nodeInsert;
return head;
}
// 定义一个临时节点用于遍历
ListNode pNode = head;
int count = 1;
// 这里的 position 被 size 限制住,所以不用考虑 pNode = null
while (count < position - 1) {
pNode = pNode.next;
count++;
}
nodeInsert.next = pNode.next;
pNode.next = nodeInsert;
return head;
}
这里补充一个点,如果 head = null 的时候,如果插入的节点是链表的头结点,也可以直接抛出不能插入的异常,两种处理都可以,一般来说我更加倾向于前者。
如果链表是单调递增的,一般会让你把元素插入到合适的地方,然后序列保持单调。
执行head=head.next即可,如下图,将head向前移动一次后,原结点不可达,会被JVM回收。
找到其前驱结点,令其指向null即可。例如下图,同样用cur.next = 40 找到其前驱结点,再执行cur.next=null即可,此时结点40不可达,被JVM回收。
同样用cur.next比较,找到位置后,将cur.next指针的值更新为cur.next.next即可。如下图:
/**
* 删除节点
*
* @param head 链表头结点
* @param position 删除节点位置,取值从 1 开始
* @return 删除后的链表头结点
*/
public static ListNode deleteNode(ListNode head, int position) {
//判断头结点是否为空,为空的话直接不删除
if (head == null) {
System.out.println("链表为空,无法删除!");
return null;
}
int size = getListLength(head);
// 删除的话一般都尾部节点就可以了,因为尾部节点的下一个节点为空,所以不需要删除
if (position > size || position < 1) {
System.out.println("输入参数有误!");
return head;
}
if(position == 1){
return head.next;
}
ListNode cur = head;
int count = 1;
while(count < position - 1){
cur = cur.next;
count++;
}
cur.next = cur.next.next;
return head;
}
package com.qinyao.linklist;
/**
* @ClassName LinkList
* @Description
* @Version 1.0.0
* @Author LinQi
* @Date 2023/09/04
*/
public class ListNode {
private int data;
private ListNode next;
public ListNode(int data) {
this.data = data;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public ListNode getNext() {
return next;
}
public void setNext(ListNode next) {
this.next = next;
}
/**
* 获取链表长度
*
* @param head 链表的头结点
* @return
*/
public static int getListLength(ListNode head) {
int length = 0;
ListNode node = head;
while (node != null) {
length++;
node = node.next;
}
return length;
}
/**
* 链表遍历打印
*
* @param head
*/
public static void printNodeList(ListNode head) {
if (head == null) {
System.out.println("链表为空,无法打印!");
return;
}
int length = 0;
//定义头结点
ListNode node = head;
while (node != null) {
System.out.println(node.data);
length++;
node = node.next;
}
}
/**
* 链表插入
*
* @param head 链表头结点
* @param nodeInsert 待插入节点
* @param position 待插入位置,从 1 开始
* @return 插入后得到的链表头结点
*/
public static ListNode insertNode(ListNode head, ListNode nodeInsert, int position) {
// 头结点为空,直接返回
if (head == null) {
return nodeInsert;
}
// 已经存放的元素格式
int size = getListLength(head);
if (position > size + 1 || position < 1) {
System.out.println("位置参数越界");
return head;
}
//表头插入
if (position == 1) {
nodeInsert.next = head;
//这里可以直接返回 nodeInsert,也可以先将头结点位置前移
head = nodeInsert;
return head;
}
// 定义一个临时节点用于遍历
ListNode pNode = head;
int count = 1;
// 这里的 position 被 size 限制住,所以不用考虑 pNode = null
while (count < position - 1) {
pNode = pNode.next;
count++;
}
nodeInsert.next = pNode.next;
pNode.next = nodeInsert;
return head;
}
/**
* 删除节点
*
* @param head 链表头结点
* @param position 删除节点位置,取值从 1 开始
* @return 删除后的链表头结点
*/
public static ListNode deleteNode(ListNode head, int position) {
//判断头结点是否为空,为空的话直接不删除
if (head == null) {
System.out.println("链表为空,无法删除!");
return null;
}
int size = getListLength(head);
// 删除的话一般都尾部节点就可以了,因为尾部节点的下一个节点为空,所以不需要删除
if (position > size || position < 1) {
System.out.println("输入参数有误!");
return head;
}
if(position == 1){
return head.next;
}
ListNode cur = head;
int count = 1;
while(count < position - 1){
cur = cur.next;
count++;
}
cur.next = cur.next.next;
return head;
}
}
构造链表的 data 以及 next 既可,data 用于存储链表的数据,next 用于指向下一个数据
首部:如果插入链表的时候,链表为空的时候,这个时候可以返回异常或者返回插入的节点就可以
中间:需要注意,需要遍历到插入的前一个节点就足够了,然后将链表对应位置的下一个节点先赋值给插入节点,然后链表的写一个节点指向插入节点既可
尾部:需要注意,如果索引大于链表元素 2 个,则表示插入错误。
首部:如果链表头部为空,无法删除
中间:删除的时候同插入,需要遍历到前一个节点
尾部:插入的时候需要注意索引位置
阅读量:2018
点赞量:0
收藏量:0