【算法】回文链表,相交链表,链表元素的删除,反转链表和思考策略(双指针,哨兵节点)

数据结构

1.链表在Java中的实现

public class Node{
    //存储当前元素的值
    public Integer value;
    //存储下一个节点的引用
    public Node next;
    public Node(Integer value){
        this.value = value;
    }
}

2.链表和数组的区别

数组:

  • 可实现随机访问,能通过索引访问到具体的值
  • 数组的内存是有限的,不能自动扩容
  • 查询快(O(1)),添加,删除,插入慢(O(n))

链表:

  • 不能实现随机访问,只能通过遍历链表的方式依次寻找
  • 链表可以自动扩容
  • 添加,删除和插入速度快(O(1)),查询慢(O(n))

3.链表的操作

1.链表的插入操作

image-20201028110654944

public static void add(Node cur,Node prev){
    cur.next = prev.next;
    prev.next = cur;
}

2.链表的删除操作

image-20201028111110278

/**
*代码演示只写了主要的逻辑过程
*因此我们默认提供了cur节点和cur节点的前序节点prev
**/
public Node delete(Node cur,Node prev){
    prev.next = cur.next;
}

注意:如果要删除的是该链表的头结点,我们只需要将头结点指向之前头结点的next就相当于删除了头结点

image-20201028112011934

3.遍历链表

  • 链表没有固定的大小,因此我们遍历链表要判断链表是否到了最后一个节点
  • 在链表中,一般用链表的头结点来代表该链表
public void loop(Node node){
	while(node !=null){
		head = head.next;
	}
}

算法

快慢指针

链表中常用的算法思想:快慢指针

快慢指针

image-20201028194707519

龟兔赛跑的问题,兔子和乌龟在同一个起点出发,乌龟每次移动一步,兔子每次移动两步,如果一个链表是有环的,那么兔子和乌龟是一定会相遇的(兔子超过乌龟一圈或者多圈)。

**注意在代码中快慢指针的起始位置不同:**因为我们链表不能使用for循环,而是使用while循环,条件是先于循环体判断的,如果设置两个指针在同一个起始点,那么直接就会跳出循环。

因此我们将慢指针设置为head(头结点),快指针设置为head.next(头结点的下一个节点)

当然也可以使用do while循环,将快慢指针都设置为头结点。

注意使用快慢指针时,需要判断边界条件:

  1. 快指针的当前节点不能是null,如果为null说明了该链表不是环形的,快指针已经到达了链表的边界
  2. 快指针的下一个节点不能是null,因为我们快指针每次都是移动两个结点,如果快指针的下一个节点是null,那么就无法指向快指针的下下个节点,造成了空指针异常。
  3. 如果使用while循环,那么头结点和头结点的下一个节点都应该判断不为null,不然快指针的赋值语句空指针异常
  4. 如果使用dowhile循环,那么只需要保证头结点不为null

哨兵节点

哨兵节点广泛应用于树和链表中,如伪头、伪尾、标记等,它们是纯功能的,通常不保存任何数据,其主要目的是使链表标准化,如使链表永不为空、永不无头、简化插入和删除。

使用【第五题 移除链表中的元素】

1.环形链表

image-20201028201322557

哈希表

思路:判断一个链表有环,我们只需要将每个节点都存入一个不允许重复的数据结构,然后就可以判断链表是否是有环的

public class Solution {
    public boolean hasCycle(ListNode head) {
        Set<ListNode> seen = new HashSet<ListNode>();
        while (head != null) {
            if (!seen.add(head)) {
                return true;
            }
            head = head.next;
        }
        return false;
    }
}

时间复杂度是O(n):最坏的情况下我们需要变量每一个节点一次。

快慢指针

public class Solution {
    public boolean hasCycle(ListNode head) {
        if(head == null || head.next == null){
            return false;
        }
        ListNode slow = head;
        ListNode fast = head.next;
        while(slow != fast){
            //关于为什么要用快指针作为判断条件
            //因为快指针一定比慢指针更快的到达边界(不是环形链表的话)
            if(fast == null || fast.next == null){
                return false;
            }
            slow = slow.next;
            fast = fast.next.next;
        }
        return true;
    }
}

2.环形链表II

image-20201028210905076

思考:

比环形链表多出的步骤是要确定何时是整个链表环的入口

对于哈希表的处理方式来讲,当再次向哈希Set中添加同样的元素的时候就是这个链表的入口,直接将该节点返回即可。

而对于快慢指针的解法,这个思考策略较为复杂。

3.相交链表

这是一道浪漫的题【错的人总会离开,对的人总会相遇】

image-20201028221209344

纯暴力

使用双层for loop遍历链表headA,看headB是否有元素出现在headA中

这个算法不太推荐,因此不书写了,知道能这样处理就可以了

哈希表

遍历链表A将节点都存在哈希表中,然后再遍历链表B,发现了重复的节点就返回,就是相交的节点。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if(headA == null || headB == null){
            return null;
        }
        Set<ListNode> set = new HashSet<ListNode>();
        while(headA != null){
            set.add(headA);
            headA = headA.next;
        }
        while(headB != null){
            if(!set.add(headB)) {
                return headB;
            }
            headB = headB.next;
        }
        return null;
    }
}

双指针

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    //tempA和tempB我们可以认为是A,B两个指针
    ListNode tempA = headA;
    ListNode tempB = headB;
    while (tempA != tempB) {
        //如果指针tempA不为空,tempA就往后移一步。
        //如果指针tempA为空,就让指针tempA指向headB(注意这里是headB不是tempB)
        tempA = tempA == null ? headB : tempA.next;
        //指针tempB同上
        tempB = tempB == null ? headA : tempB.next;
    }
    //tempA要么是空,要么是两链表的交点
    return tempA;
}

4.反转链表

image-20201031210434275

分析:

让快指针的节点的指针指向慢节点,然后快慢节点都往链表后移(需要先将快节点的下一个指针保存下来)

class Solution {
    public ListNode reverseList(ListNode head) {
        //可以加个特殊判断
        if(head == null || head.next == null){
            return head;
        }
        ListNode pre = head;
        ListNode cur = head.next;
        while(head != null) {
            ListNode temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }
    }
}

5.移除链表中的元素

image-20201031203833135

分析:删除的是链表中节点值等于val的所有的节点,返回的是删除之后的链表

思路:

  • 删除链表中的所有val的节点--------遍历链表的所有元素
  • 如何删除--------判断节点的val值,相等就删除,不相等就指针后移一位
  • 特别注意:很可能会删除的节点是第一个,那么这是我们为了保证链表一定是有头的,这里使用了哨兵节点的技术
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        ListNode myhead = new ListNode(0);
        myhead.next = head;
        
        ListNode pre = myhead;
        ListNode cur = head;
        while(cur != null){
            if(cur.val == val){
                pre.next = cur.next;
            }else{
                pre = cur;
            }
            cur = cur.next;
        }
        //返回myhead的下一个节点,因为之前的head节点的值可能会被删除了,这时如果返回head,那么就会返回[]
        return myhead.next;
    }
}

要注意在链表中删除,插入添加等操作时为了保证链表一定是有头的或者一定是有尾的可以使用哨兵节点的技术,哨兵节点并没有任何实际的意义,仅仅是功能意义,为了操作方便而已。

就类似于有的题目会自带一个头结点,而头结点是不含任何的信息的。

6.回文链表

image-20201031210524787

转化为判断数组是否是回文数组

  • 将链表中的值都存入数组中(要创建的是动态数组)
  • 双指针数组判断回文数组(头尾指针)

数组是可以通过索引取值的,利用的是内存寻址系统

class Solution {
    public boolean isPalindrome(ListNode head) {
        List<Integer> list = new ArrayList<Integer>();
        while(head != null){
            list.add(head.val);
            head = head.next;
        }
        int fomer = 0;
        int back = list.size() - 1;
        while(fomer < back){
            if(!list.get(fomer).equals(list.get(back))){
                return false;
            }
            fomer++;
            back--;
        }
        return true;
    }
}

7.删除链表中的节点

image-20201031211215933

分析:给一个节点,然后让删除该节点,并且该节点不是末尾的节点,那么该节点一定是有尾节点的.

思路:直接将该节点的信息改为了它的下一个节点的信息。

class Solution {
    public void deleteNode(ListNode node) {
        node.val = node.next.val;
        node.next = node.next.next;
    }
}

8.删除链表的节点II

image-20201031211657948

分析:

条件:

  • 给了链表和头结点,和一个int值,该int值是节点中存储的数据
    • 删除的节点的位置不固定,可能是链表中的任意位置(可能是头,也可能是尾)

思路1:搬移数据改变指针指向

  • 遍历链表找到节点的位置
  • 改变要删除的值的为下一个节点的值,其指向对应改变
class Solution {
    public ListNode deleteNode(ListNode head, int val) {
        //        ListNode myhead = new ListNode(0);
//        myhead.next = head;
//        while (head != null){
//            if (head.val == val){
//                head.val = head.next.val;
//                head.next = head.next.next;
//            }else{
//                head = head.next;
//            }
//        }
//        return myhead.next;
    }
}
  • 该解法思路是对的,但是细节会发生错误,假如要删除的节点是最后一个节点就会发生错误,空指针

思路2:使用双指针改变指针指向

class Solution {
    public ListNode deleteNode(ListNode head, int val) {
        if(head == null){
            return head;
        }
        //使用哨兵节点,保证链表一定是有头的
        //这样假如删除的是第一个节点,我们也能正常返回
        ListNode myhead = new ListNode(0);
        myhead.next = head;
        
        ListNode pre = myhead;
        ListNode cur = head;
        while(cur != null){
            if(cur.val == val){
                pre.next = cur.next;
            }else{
                pre = cur;
            }
            cur = cur.next;
        }
        return myhead.next;
    }
}

总结:

  • 删除时要判断是否删除节点的位置
    • 有可能删除头就用哨兵
    • 有可能删除尾就用双指针
    • 如果不可能删除尾可以直接搬移数据,改变指针指向
        ListNode pre = myhead;
        ListNode cur = head;
        while(cur != null){
            if(cur.val == val){
                pre.next = cur.next;
            }else{
                pre = cur;
            }
            cur = cur.next;
        }
        return myhead.next;
    }
}

总结:

  • 删除时要判断是否删除节点的位置
    • 有可能删除头就用哨兵
    • 有可能删除尾就用双指针
    • 如果不可能删除尾可以直接搬移数据,改变指针指向
  • 是删除一个节点还是所以的节点
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在Python中,可以使用快慢指针法来判断一个链表是否为回文链表。快慢指针法的思路是,使用两个指针同时从链表的头部出发,快指针每次向后移动两个位置,慢指针每次向后移动一个位置。当快指针到达链表末尾时,慢指针刚好指向链表的中间位置。 具体的实现步骤如下所示: 1. 创建两个指针slow和fast,初始都指向链表的头部。 2. 使用while循环,当fast的下一个节点和下下个节点都不为空时,执行循环体。 - 慢指针slow每次向后移动一个位置,即slow = slow.next。 - 快指针fast每次向后移动两个位置,即fast = fast.next.next。 3. 循环结束后,慢指针slow将指向链表的中间位置。注意,如果链表长度为奇数,slow指向的是正中间的节点;如果链表长度为偶数,slow指向的是后半段的第一个节点。 4. 将中间节点的下一个节点开始的链表进行倒序,可以使用一个辅助函数reverse来实现。reverse函数的作用是将一个链表倒序并返回新的链表头部。 5. 将slow的next指针置为None,即切断链表中间位置的节点与后半段链表的连接。 6. 使用两个指针p和q分别指向原链表的头部和倒序后的链表的头部。然后同时向后遍历这两个链表,比较节点的值是否相等。如果出现不相等的情况,说明链表不是回文链表,返回False。 7. 当遍历结束后,如果q指针为None,说明两个链表长度相等且所有节点的值都相等,返回True;否则,链表不是回文链表,返回False。 下面是使用快慢指针法判断回文链表的Python代码实现: ``` # 是否是回文 def palindrome(head): if head is None: return True slow = fast = head while fast.next is not None and fast.next.next is not None: slow = slow.next fast = fast.next.next mid = slow.next reversed_mid = reverse(mid) slow.next = None p = head q = reversed_mid while p is not None and q is not None: if p.val != q.val: return False p = p.next q = q.next return True # 辅助函数:链表倒序 def reverse(head): if head is None or head.next is None: return head pre = None cur = head while cur is not None: next_node = cur.next cur.next = pre pre = cur cur = next_node return pre ``` 通过以上代码,我们可以使用快慢指针法来判断一个链表是否为回文链表。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

炒冷饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值