[数据结构] 链表详解

链表可是很重要的知识,是面试时常考的知识点,这次让我们系统的学习一下吧



1. 链表的定义

一个链表就像火车一样,由一个个节点串起来,每个节点都要标出节点值和它的下一个节点的物理位置(next),之后再一次连接,就组成了链表.
链表有几种常见类型,单向循环带头节点,单向循环不带头结点,单项非循环带头节点,单向非循环不带头结点,双向循环带头结点…
这里,我们只讲最重要最常考的两个双向非循环不带头结点和单项非循环不带头结点.
如下图,链表的一个节点由节点值val和它的下一节点的位置组成

在这里插入图片描述
如下图是一个单向不循环无头结点的链表.链表的第一个节点,0x21是节点自身位置,12是节点值,0x39是下一个节点的位置,也就是第二个节点的位置.
在这里插入图片描述
链表的空间顺序,逻辑上是连续的,物理上是随机的.
而顺序表ArrayList物理上是连续存储的一块空间.

2. 链表的创建

2.1 基础创建

1.我们先简单创建一个链表,熟悉一下链表的使用,注意结尾处的this.head = listnode1,指定第一个节点为链表头节点.

 	public void createList(){
        ListNode listnode1 = new ListNode(12);
        ListNode listnode2 = new ListNode(25);
        ListNode listnode3 = new ListNode(3);
        ListNode listnode4 = new ListNode(79);
        ListNode listnode5 = new ListNode(53);
        listnode1.next = listnode2;
        listnode2.next = listnode3;
        listnode3.next = listnode4;
        listnode4.next = listnode5;
        listnode5.next = null;
        this.head = listnode1;
    }

2.2 尾插法创建头节点

尾插法就是把新节点插到链表的末尾.
如下图所示,要求插入新的节点,节点值为99.
在这里插入图片描述
首先,我们要讨论一种特殊情况,就是链表还没有节点,那么头节点就是这个插入的新节点.

ListNode listnode = new ListNode(data);
if(head == null){
    head = listnode;
}

若是节点有头节点,则就遍历链表,找到链表尾巴,把新节点插到尾巴后面即可.
那么怎么找到链表的尾巴呢,从那面那幅图我们看到,链表尾巴的next为null,所以从头开始遍历,直到节点的next为null.就把新节点插到这个节点后面.

			ListNode cur = head;
            while(cur.next != null){
                cur = cur.next;
            }
            cur.next = listnode;

完整代码如下

	public void addLast(int data){
        ListNode listnode = new ListNode(data);
        if(head == null){
            head = listnode;
        }else{
            ListNode cur = head;
            while(cur.next != null){
                cur = cur.next;
            }
            cur.next = listnode;
            listnode.next = null;
        }
    }

执行结果为
在这里插入图片描述

2.3 头插法

如下图,把节点值为99的节点插在链表头部,即node.next = head;head改为node,这里没有解引用,不用判断head == null.
在这里插入图片描述

	public void addFirst(int data){
        ListNode node = new ListNode(data);
        node.next = head;
        head = node;
    }

在这里插入图片描述

3. 链表的基础方法

3.1 获取链表长度

遍历一遍链表,从head开始,注意结束位置,是cur != null,注意别落下最后一个节点.

	public int size(){
        int count = 0;
        ListNode cur = head;
        while(cur != null){
            count++;
            cur = cur.next;
        }
        return count;
    }

3.2 是否包含某个节点

注意循环条件,cur != null

	public boolean contain(int key){
        ListNode cur = this.head;
        while(cur != null){
            if(cur.val == key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

3.3 在任意坐标处插入节点

为了避免程序在遇到不合法的输入时崩溃,我们在启动程序之前就要考虑好所有可能输入值会出现的结果.
void addIndex(int index, int value)
首先,若是选定的坐标小于0,或者超出给定范围,程序要报错,防止程序崩溃.

		if(index < 0 || index > size()){
            throw new WrongIndexException("选择的坐标不合法");
        }

其次,当给定坐标是0时,头插此节点,给定坐标是size()时,尾插此节点

	   if(index == 0){
            addFirst(value);
        }else if(index == size()){
            addLast(value);
        }

若都不是,如下图,在坐标1处插入值为100的节点.
在这里插入图片描述
这里就要修改99的next指向和100的next指向.也就是找到坐标处的前一个位置cur,如下图在这里插入图片描述
注意这里我们不可以先修改cur的next,如下代码,如果先修改cur的next,node的next就找不到了.

cur.next = node;
node.next = cur.next

正确的写法如下

node.next = cur.next;
cur.next = node;

完整代码如下

	public void addIndex(int index,int value){
        if(index < 0 || index > size()){
            throw new WrongIndexException("选择的坐标不合法");
        }
        if(index == 0){
            addFirst(value);
        }else if(index == size()){
            addLast(value);
        }else{
            ListNode cur = head;
            while((index-1) > 0){
                cur = cur.next;
                index--;
            }
            ListNode node = new ListNode(value);
            node.next = cur.next;
            cur.next = node;
        }
    }

3.4 删除第一个值为key的节点

首先,我们需要找到值为key的节点的前一个结点,改变这个节点的next指向,就可以删除值为key的节点.
这里要考虑几个特殊情况
1.head 为 null,链表为空链表,直接返回

		if (head == null) {
            System.out.println("链表为空链表,无法删除");
            return;
        }

2.我们遍历的节点值,是从head.next开始的,所以要特别关注一下head的值是不是key,若head.val 为key,直接head = head.next;

		ListNode cur = head;
        //如果头节点的值 == key
        if(cur.val == key){
            head = head.next;
            return;
        }

之后,正常找,找到值为key的节点的前一个结点.

		while (cur.next != null) {
            if (cur.next.val == key) {
                break;
            }
            cur = cur.next;
        }

如果遍历完毕,找到了,修改前一个结点的next指向,没找到,直接返回

		if(cur.next != null){//正常找到,改变cur的next指向即可
            cur.next = cur.next.next;
        }else{               
            System.out.println("未找到要删除的节点");//没找到节点,直接返回
            return;
        }

完整代码如下

	public void deleteKey(int key) {
        if (head == null) {
            System.out.println("链表为空链表,无法删除");
            return;
        }
        ListNode cur = head;
        //如果头节点的值 == key
        if(cur.val == key){
            head = head.next;
            return;
        }
            //找到值为key的节点的前一个结点,找到后退出循环
        while (cur.next != null) {
            if (cur.next.val == key) {
                break;
            }
            cur = cur.next;
        }
        //如果刚好是最后一个节点值为key,删除最后一个节点,即改变preCur的next指向
        if(cur.next != null){//正常找到,改变cur的next指向即可
            cur.next = cur.next.next;
        }else{               
            System.out.println("未找到要删除的节点");//没找到节点,直接返回
            return;
        }
    }

3.5 删除所有节点值为key的节点

这道题有点难想,我们需要找到每一个节点为key的节点的前一个结点,改变节点的next指向,进而删除节点.
如下图,删除所有值为12的节点.从第二个节点开始遍历,若是cur的值为key,便修改preCur的next指向.否则,cur和preCur往后走.
在这里插入图片描述
第二个节点值不为key,cur,preCur往后走.如下图
在这里插入图片描述
cur的值为key,便通过修改preCur的next指向来删除第三个节点.preCur.next = cur.next;cur向后移动.注意这里preCur是记录链表cur节点的前一个节点,所以这里preCur不用变.

 			if(cur.val == key){
                preCur.next = cur.next;
                cur = cur.next;
              }

如下图
在这里插入图片描述
之后的操作重复,不做赘述.
最后,注意我们还没比较头节点的值是不是key,如果符合的话,需要删除头节点,head = head.next;

		 if(head.val == key){
            head = head.next;
        }

完整代码如下.

	public void deleteAllKey(int key){
        if(head == null){
            System.out.println("链表为空链表");
            return;
        }
        ListNode cur = head.next;
        ListNode preCur = head;
        while(cur != null){
            if(cur.val == key){
                preCur.next = cur.next;
                cur = cur.next;
            }else{
                preCur = cur;
                cur = cur.next;
            }
        }
        if(head.val == key){
            head = head.next;
        }
    }

4. 链表的进阶方法

4.1 反转链表

如下图所示,将链表反转
在这里插入图片描述
思路是使后一节点的next指向前一节点,但要事先保存好后一节点的next节点,防止改变后,丢失原来的next结点.
先考虑两个特殊情况,空链表和只有一个节点的链表直接返回就可.

		if(head == null){
            return ;
        }
        if(head.next == null){
            return ;
        }

定义cur = head.next,从cur开始遍历,preCur = head,记录前一个结点,是cur的next指向自己的preCur.nextCur = cur.next;保存好cur的next.如下图
在这里插入图片描述

修改第二个节点指向

		    nextCur = cur.next;
            cur.next = preCur;
            preCur = cur;
            cur = nextCur;

如下图,修完完第二个节点的next指向,cur = nextCur.所以一定要事先记录好cur的原next节点.
在这里插入图片描述
之后,再次进入循环,记录完cur的next节点后,修改cur的next指向.
最后全部修改完毕,如下图,注意处理头节点和尾节点.
原头节点12的next置为空,之后将头节点head 设置为preCur

在这里插入图片描述
完整代码如下

	public void reverseList(){
        if(head == null){
            return ;
        }
        if(head.next == null){
            return ;
        }
        ListNode preCur = head;
        ListNode cur = head.next;
        ListNode nextCur = null;
        while(cur != null){
            nextCur = cur.next;
            cur.next = preCur;
            preCur = cur;
            cur = nextCur;
        }
        head.next = null;
        head = preCur;
    }

执行结果如下
在这里插入图片描述

4.2 找出中间节点

这个我们通过快慢节点的方法找出中间节点.快节点一次走两步,慢节点一次走一步,快节点走到终点的时候,列出方程,2x = s,x = s/2,则慢节点走到了中间位置.
首先,如果是空链表或者是只有一个节点,直接返回head即可.

		if(head == null){
            System.out.println("链表为空链表");
            return null;
        }
        if(head.next == null){
            return head;
        }

之后,快慢节点往后走,直到fast.next = null.

		while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
        }

如下图,slow节点就是中间节点.
在这里插入图片描述
完整代码如下

 	public ListNode getMidNode(){
        if(head == null){
            System.out.println("链表为空链表");
            return null;
        }
        if(head.next == null){
            return head;
        }
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }

执行结果为
在这里插入图片描述

4.3 找出倒数第K个节点

用的方法也是快慢节点,快节点先走K-1步,之后快慢节点一起走,快节点走到终点,慢节点就到了倒数第K个节点.
如下图,找倒数第4个节点,快节点先走3步
在这里插入图片描述
之后,快慢节点一起走,直到快节点走到终点,慢节点就是倒数第四个节点.
在这里插入图片描述
首先讨论特殊情况
1.链表为空链表,直接返回null
2.K非法,小于0,或者大于链表的size().需要抛出异常.

		if(head == null){
            return null;
        }
        if(k < 0 || k > this.size()){
            throw new WrongIndexException("输入的坐标非法");
        }

正常情况,fast先走K-1,之后fast,slow一起走

	ListNode fast = head;
        ListNode slow = head;
        while(k-1 > 0){
            fast = fast.next;
            k--;
        }
        while(fast.next != null){
            fast = fast.next;
            slow = slow.next;
        }

完整代码如下

public ListNode LastKthNode(int k){
        if(head == null){
            return null;
        }
        if(k < 0 || k > this.size()){
            throw new WrongIndexException("输入的坐标非法");
        }
        ListNode fast = head;
        ListNode slow = head;
        while(k-1 > 0){
            fast = fast.next;
            k--;
        }
        while(fast.next != null){
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

4.4 用数值x分割列表

链表小于数x的节点放左边,大于x的节点放右边,节点相对顺序要求保持不变.
如下图,要求节点值小于30的节点放左边,节点值大于30的节点放右边.保持相对位置不变,例如第二个链表中,54要在44的前面,这个相对位置不能变.
在这里插入图片描述
这里我们需要实现两个链表,第一个链表装小于30的节点,第二个链表装大于30的节点,最后再将两个链表连起来,就可以啦~
所以呢,我们这里要准备第一个节点的尾节点be和第二个节点的头节点as,用于两个链表的连接.准备第一个链表的头节点,用于新链表的遍历,准备第二个节点的尾节点,给他的next置空.如下图所示.
在这里插入图片描述
首先,找到第一个小于30的节点,作为链表1的头节点bs,之后小于30的节点往后连.

		 if(cur.val < x){
                if(bs == null){
                    bs = cur;
                    be = cur;
                }else{
                    be.next = cur;
                    be = be.next;
                }
            }

找到第一个大于30的节点,作为链表2的头节点,之后大于30的节点往后连.

			else{
                if(as == null){
                    as = cur;
                    ae = cur;
                }else{
                    ae.next = cur;
                    ae = ae.next;
                }
            }

最后将这两个链表连起来.
这里要考虑一个特殊情况,原链表里如果没有小于30的节点,bs = null,直接返回as就行,记得要把ae的next置空.

		if(bs == null){
           if(ae != null){
               ae.next = null;
           }
            return as;
        }

正常情况,直接连就可以.

		else{
            be.next = as;
            if(ae != null){
                ae.next = null;
            }
            return bs;
        }

完整代码如下

	public ListNode seperateList(int x){
        ListNode bs = null;
        ListNode be = null;
        ListNode as = null;
        ListNode ae = null;
        ListNode cur = head;
        while(cur != null){
            if(cur.val < x){
                if(bs == null){
                    bs = cur;
                    be = cur;
                }else{
                    be.next = cur;
                    be = be.next;
                }
            }else{
                if(as == null){
                    as = cur;
                    ae = cur;
                }else{
                    ae.next = cur;
                    ae = ae.next;
                }
            }
            cur = cur.next;
        }
        if(bs == null){
           if(ae != null){
               ae.next = null;
           }
            return as;
        }else{
            be.next = as;
            if(ae != null){
                ae.next = null;
            }
            return bs;
        }
    }

4.5 判断是否为回文链表

如下图,两个链表都为回文链表.
在这里插入图片描述
在这里插入图片描述

如何判断链表是否是回文链表呢?
我们可以发现,回文链表的首尾节点到中间节点的值一直是相同的.我们首先找到中间节点,反转后面的链表,如下图,我们从头节点和尾节点向中间遍历,如果节点值一直相同,则为回文链表,否则不是.
在这里插入图片描述
找到中间节点,之前的代码说过这个,用什么方法来着?---------------------------快慢指针,slow为中间节点.

		ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
        }

反转中间节点之后的链表

		ListNode cur = slow.next;
        ListNode preCur = slow;
        while(cur != null){
            ListNode curNext = cur.next;
            cur.next = preCur;
            preCur = cur;
            cur = curNext;
        }

被反转后的链表如下图.
在这里插入图片描述
在这里插入图片描述

这里的preCur就成了尾节点了.
之后从首尾向中间遍历节点,值不同返回false,直到首尾节点都走到了中间节点(节点数为双数)或者preCur.next = head()节点数为单数,循环结束.

		while(preCur != head){
            if(preCur.val != head.val){
                return false;
            }
            if(preCur.next == head){
                return true;
            }
            preCur = preCur.next;
            head = head.next;
        }
        return false;

完整代码

	public boolean palindromeLinkedList(){
        if(head == null){
            return false;
        }
        if(head.next == null){
            return true;
        }
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
        }
        ListNode cur = slow.next;
        ListNode preCur = slow;
        while(cur != null){
            ListNode curNext = cur.next;
            cur.next = preCur;
            preCur = cur;
            cur = curNext;
        }
        while(preCur != head){
            if(preCur.val != head.val){
                return false;
            }
            if(preCur.next == head){
                return true;
            }
            preCur = preCur.next;
            head = head.next;
        }
        return false;
    }

4.6 判断链表是否有环

如下图,链表最后一个节点指向前面的某个节点,则链表中出现了环.
在这里插入图片描述
如何判断链表是否有环呢?
我们采用快慢指针的方法,由于链表中有环,一个走两步,一个走一步,两个指针总会指向同一个节点.
完整代码如下.

	public boolean linkedListHasRing(){
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow){
                return true;
            }
        }
        return false;
    }

1. 附加 给链表添加环

最后一个节点的next指向前面的某个节点.

	public void createRing(){
        ListNode cur = head;
        while(cur.next != null){
            cur = cur.next;
        }
        cur.next = head.next;
    }

4.7 找出环的入口

这里用到了数学里的解方程,如下图,
慢指针的路程*2 = 快指针的路程
如下图推导过程,推导出:头节点到环入口的距离 = 相遇点到环入口的距离

在这里插入图片描述
所以,先找到相遇点,让指针分别从头节点和相遇点往中间走,直到两指针相遇,相遇的点就是环的入口.

找到相遇点

		ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow){
                break;
            }
        }

如果链表没环,循环是不符合循环条件之后,自然跳出来的

		if(fast == null || fast.next == null){
            return null;
        }

链表有环的话,让两个指针从头节点和相遇点往中间走,相遇的点为环的入口.

		ListNode cur = head;
        while(cur != slow){
            cur = cur.next;
            slow = slow = slow.next;
        }
        return cur;

完整代码

	public ListNode inletOfRing(){
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow){
                break;
            }
        }
        if(fast == null || fast.next == null){
            return null;
        }
        ListNode cur = head;
        while(cur != slow){
            cur = cur.next;
            slow = slow = slow.next;
        }
        return cur;
    }

本文结束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值