链表可是很重要的知识,是面试时常考的知识点,这次让我们系统的学习一下吧
文章目录
- 1. 链表的定义
 - 2. 链表的创建
 - 2.1 基础创建
 - 2.2 尾插法创建头节点
 - 2.3 头插法
 
- 3. 链表的基础方法
 - 3.1 获取链表长度
 - 3.2 是否包含某个节点
 - 3.3 在任意坐标处插入节点
 - 3.4 删除第一个值为key的节点
 - 3.5 删除所有节点值为key的节点
 
- 4. 链表的进阶方法
 - 4.1 反转链表
 - 4.2 找出中间节点
 - 4.3 找出倒数第K个节点
 - 4.4 用数值x分割列表
 - 4.5 判断是否为回文链表
 - 4.6 判断链表是否有环
 - 1. 附加 给链表添加环
 
- 4.7 找出环的入口
 
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,注意别落下那个cur.next == 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节点.
 先考虑两个特殊情况,空链表和只有一个节点的链表直接返回就可.
		if(head == null){
            return ;
        }
        if(head.next == null){
            return ;
        }
 
定义cur = head.next,preCur = head,nextCur = 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;
    }
 

















