算法与数据结构——算法基础——链表(java)(左程云b站课程笔记总结)

链表

方法论

  • 对于笔试,不用太在乎空间复杂度,一切为了时间复杂度
  • 对比面试,时间复杂度依然放在第一位,但是需要找到空间最省的方法
  • 重要技巧
    • 额外数据结构记录(哈希表等)
    • 快慢指针

单链表、双链表基本结构

value next和value next last

反转单向链表

【如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1)】

栈(若没有额外空间复杂度要求)

public ListNode reverseList(ListNode head){
    Stack<ListNode> stack=new Stack<>();
    while(head!=null){
        stack.push(head);
        head=head.next;
    }
    if(stack.isEmpty()){
        return null;
    }
    ListNode node=stack.pop();
    ListNode dummy=node;
    while(!stack.isEmpty()){
        ListNode tempNode=stack.pop();
        node.next=tempNode;
        node=node.next;
    }
    //最后一个结点就是反转前的头结点,一定要让他的next
    //等于空,否则会构成环
    node.next=null;
    return dummy;
}

递归

public ListNode reverseListNode(ListNode head){
    if(head==null||head.next==null){
        return head;
    }
    ListNode newHead=reverseListNode(head.next);
    head.next.next=head;
    head.next=null;
    return newHead;
}

左神

把原链表的结点一个个摘掉,每次摘掉的链表都让他成为新的链表的头结点,然后更新新链表

image-20220604153333756

public ListNode reverseListNode(ListNode head){
    ListNode pre=null;
    ListNode next=null;
    while(head!=null){
        next=head.next;
        head.next=pre;
        pre=head;
        head=next;
    }
    return pre;
}

反转单向链表Ⅱ

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

穿针引线

image-20220604153247644

class Solution {
    public ListNode reverseBetween(ListNode head, int left, int right) {
        ListNode dummyNode=new ListNode(-1);//一般操作
        dummyNode.next=head;
        ListNode pre=dummyNode;
        for(int i=1;i<left;i++){
            pre=pre.next;
        }
        ListNode cur=pre.next;
        ListNode next=null;
        for(int i=0;i<right-left;i++){
            next=cur.next;
            cur.next=next.next;
            next.next=pre.next;
            pre.next=next;
        }
        return dummyNode.next;
    }
}

反转双向链表

【如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1)】

public DoubleNode reverseDoubleNode(DoubleNode head){
    DoubleNode pre=null;
    DoubleNode next=null;
    while(head!=null){
        next=head.next;
        head.next=pre;
        head.last=next;
        pre=head;
        head=next;
    }
    return pre;
}

打印两个有序链表的公共部分

要求:如果两个链表的长度之和为N,时间复杂度要求为O(N),额外空间复杂度为O(1)

谁小谁移动,相等就一起移动并打印(保存),一个越界了就停

public static void printCommonPart(Node head1, Node head2) {
    System.out.print("Common Part: ");
    while (head1 != null && head2 != null) {
        if (head1.value < head2.value) {
            head1 = head1.next;
        } else if (head1.value > head2.value) {
            head2 = head2.next;
        } else {
            System.out.print(head1.value + " ");
            head1 = head1.next;
            head2 = head2.next;
        }
    }
    System.out.println();
}

判断一个链表是否是回文链表

笔试:栈

全部进栈,出栈的时候就相当于是反着读,与正着读的时候比较即可

public boolean isPalidromeList(ListNode head){
    Stack<ListNode> stack=new Stack<>();
    ListNode cur=head;
    while(cur!=null){
        stack.push(cur);
        cur=cur.next;
    }
    while(head!=null){
        if(head.value!=stack.pop().value){
            return false;
        }
        head=head.next;
    }
    return true;
}

优化

快慢指针

快指针一次走两步,慢指针一次走一步,快指针走完的时候满指针在中点

只需要存一半进栈

1.奇数时慢指针出现在中点,偶数时满指针出现在第一个

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

2.奇数时出现在中点的下一个,偶数时出现在第二个

ListNode fast=head.next;
ListNode slow=head;
while(fast.next!=null&&fast.next.next!=null){
    slow=slow.next;
    fast=fast.next.next;
}
public boolean isPalindrome(ListNode head){
    if(head==null||head.next==null){
        return true;
    }
    ListNode right=head.next;
    ListNode cur=head;
    while (cur.next != null && cur.next.next != null) {
        right = right.next;
        cur = cur.next.next;
    }
    Stack<Node> stack = new Stack<Node>();
    while (right != null) {
        stack.push(right);
        right = right.next;
    }
    while (!stack.isEmpty()) {
        if (head.value != stack.pop().value) {
            return false;
        }
        head = head.next;
    }
    return true;
}

如何做到额外空间复杂度为O(1)

笔试的做法

快慢指针+反转+对比+恢复

public boolean isPalindrome(ListNode head){
    if(head==null||head.next==null){
        return true;
    }
    ListNode fast=head;
    ListNode slow=head;
    while(fast.next!=null&&fast.next.next!=null){
        slow=slow.next;
        fast=fast.next.next;
    }
    fast=slow.next;
    slow.next=null;// mid.next -> null
    ListNode next=null;
    //反转
    while(fast!=null){
        next=fast.next;
        fast.next=slow;
        slow=fast;
        fast=next;
    }
    next=slow;//save last node
    fast=head;
    boolean res=true;//后面还要恢复链表
    while(slow!=null&&fast!=null){
        if(slow.value!=fast.value){
            res=false;
            break;
        }
        slow=slow.next;
        fast=fast.next;
    }
    slow=next.next;
    next.next=null;
    while(slow!=null){
        fast=slow.next;
        slow.next=next;
        next=slow;
        slow=fast;  
    }
    return res;
}

将单向链表按某个值划分成左边小,中间相等,右边大的形式

笔试:先将链表的每一个节点放到数组中,在数组上进行partition,再按链表串起来

public ListNode listPartition(ListNode head,int num){
    if(head==null){
        return head;
    }
    ListNode cur=head;
    int i=0;
    
    //记录长度
    while(cur!=null){
        i++;
        cur=cur.next;
    }
    ListNode[] nodeArr=new Node[i];
    i=0;
    cur=head;
    for(int i=0;i!=nodeArr.length;i++){
        nodeArr[i]=cur;
        cur=cur.next;
    }
    arrPartition(nodeArr,num);
    for(i=1;i!=nodeArr.length;i++){
        nodeArr[i-1].next=nodeArr[i];
    }
    nodeArr[i-1].next=null;
    return nodeArr[0];
}

public void arrPartition(ListNode[] nodeArr,int num){
    int small=-1;
    int big=nodeArr.length;
    int index=0;
    while(index!=big){
        if(nodeArr[index].value<num){
            swap(nodeArr,++small,index++);
        }else if(nodeArr[index].value==num){
            index++
        }else{
            swap(nodeArr,--big,index);
        }
    }
}

public void swap(ListNode[] nodeArr,int a,int b){
    Node tmp=nodeArr[a];
    nodeArr[a]=nodeArr[b];
    nodeArr[b]=tmp;
}

面试:在链表上直接partition(六个变量)可以不改变相对次序,即保持稳定性且没有额外的空间

注:最后将三个区域合并时,需要讨论清楚边界,避免出现该区域没值的情况

public ListNode listPartition(ListNode head,int num){
    ListNode sH=null;
    ListNode sT=null;
    ListNode eH=null;
    ListNode eT=null;
    ListNode bH=null;
    ListNode bT=null;
    ListNode next=null;
    
    while(head!=null){
        next=head.next;
        head.next=null;
        if(head.value<num){
            if(sH==null){
                sH=head;
                sT=head;
            }else{
                sT.next=head;
                sT=head;
            }
        }else if(head.value==num){
            if(eH==null){
                eH=head;
                eT=head;
            }else{
                eT.next=head;
                eT=head;
            }
        }else{
            if(bH==null){
                bH=head;
                bT=head;
            }else{
                bT.next=head;
                bT=head;
            }
        }
        head=next;
    }
    if(sT!=null){
        sT.next=eH;
        eT=eT==null?sT:eT;
    }
    if(eT!=null){
        eT.next=bH;
    }
    return sH!=null?sH:eH!=null?eH:bH;
    
}

复制含有随机指针节点的链表

image-20220605140611359

笔试:哈希表,key存老的node,value存新的node

public ListNode copyListNodeWithRan(ListNode head){
    HashMap<ListNode,ListNode> hashmap=new HashMap<>();
    ListNode cur=head;
    while(cur!=null){
        hashmap.put(cur,new ListNode(cur.value));
        cur=cur.next;
    }
    cur=head;
    while(cur!=null){
        hashmap.get(cur).next=hashmap.get(cur.next);
        hashmap.get(cur).rand=hashmap.get(cur.rand);
        cur=cur.next;
    }
    return hashmap.get(head);
}

通过克隆节点的位置(放在原节点的下一个)可以把HashMap省掉

合并——>复制——>分离

public ListNode copyListNodeWithRand(ListNode head){
    if(head==null){
        return null;
    }
    ListNode cur=head;
    ListNode next=null;
    // copy node and link to every node
    while(cur!=null){
        next=cur.next;//recording
        cur.next=new Node(cur.value);
        cur.next.next=next;
        cur=next;
    }
    cur=head;
    ListNode curCopy=null;
    // set copy node rand
    while(cur!=null){
        next=cur.next.next;
        curCopy=cur.next;
        curCopy.rand=cur.rand!=null?cur.rand.next:null;
        cur=next;
    }
    ListNode res=head.next;
    cur=head;
    //split
    while(cur!=null){
        next=cur.next.next;
        curCopy=cur.next;
        cur.next=next;
        curCopy.next=next!=null?next.next:null;
        cur=next;
    }
    return res;
}

两个单链表相交的一系列问题

image-20220605144125670

  1. 首先实现一个函数判断一个单链表是否有环,有的话返回环的入环节点loop,否则返回null(两条链表分别调用该函数)

    • 用哈希表HashSet实现,有额外空间,遍历并插入,若在遍历结束之前有无法插入的,就返回

    • 快慢指针,无额外空间

      快指针一次走两步,慢指针一次走一步,如果无环,快指针会到null,如果有环,快慢指针必会相遇

      相遇之后,快指针回到头节点,然后快慢指针同时,每次走一步,最终会在入环节点相遇

  2. 分类讨论

    1. 两个链表都无环,如果相交,相交的时刻到走到结尾都是共有的。分别遍历两个链表得到len1,end1和len2,end2,如果end1与end2内存地址不相等的话不可能相交,相等的话找出相交部分的第一个节点,让长度大的先走,直到和长度小的长度一致,然后一起走

    2. 一个有环,一个没环,这种情况两条链表不可能相交

    3. 两个链表都有环

      image-20220605153421076

      • 当loop1和loop2内存地址相同,就是第二种情况,把入环节点当成终止节点,然后操作和1相同
      • 让loop1继续往下走在回到自己之前如果能遇到loop2就是第三种情况(随便返回其中一个loop都行),否则就是第一种情况,返回null
public ListNode getIntersectNode(ListNode head1,ListNode head2){
    if(head1==null||head2==null){
        return null;
    }
    ListNode loop1=getLoopNode(head1);
    ListNode loop2=getLoopNode(head2);
    if(loop1==null&&loop2==null){
        return noLoop(head1,head2);
    }
    if(loop1!=null&&loop2!=null){
        return bothLoop(head1,loop1,head2,loop2);
    }
    //其他情况包括一个有环一个无环的情况d
    return null;
}

//如果有环,获取入环节点
public ListNode getLoopNode(ListNode head){
    if(head==null||head.next==null||head.next.next==null){
        return null;
    }
    ListNode n1=head.next;
    ListNode n2=head.next.next;
    //防止后续进不去循环
    while(n1!=n2){
        if(n2.next==null||n2.next.next==null){
            return null;
        }
        n2=n2.next.next;
        n1=n1.next;
    }
    n2=head;
    while(n1!=n2){
        n1=n1.next;
        n2=n2.next;
    }
    return n1;
}
//如果都没有环的情况
public ListNode noLoop(ListNode head1,ListNode head2){
    if(head1==null||head2==null){
        return null;
    }
    ListNode cur1=head1;
    ListNode cur2=head2;
    int n=0;
    while(cur1.next!=null){
        n++;
        cur1=cur1.next;
    }
    while(cur2.next!=null){
        n--;
        cur2=cur2.next;
    }
    if(cur1!=cur2){
        return null;
    }
    cur1=n>0?head1:head2;
    cur2=cur1==head1:haed2:head1;
    n=Math.abs(n);
    while(n!=0){
        n--;
        cur1=cur1.next;
    }
    while(cur1!=cur2){
        cur1=cur1.next;
        cur2=cur2.next;
    }
    return cur1;
}
//如果都有环的情况
public ListNode bothLoop(ListNode head1,ListNode loop1,ListNode head2,ListNode loop2){
    ListNode cur1==null;
    ListNode cur2==null;
    if(loop1==loop2){
        cur1=head1;
        cur2=head2;
        int n=0;
        while(cur1!=loop1){
            n++;
            cur1=cur1.next;
        }
        while(cur2!=loop2){
            n--;
            cur2=cur2.next;
        }
        cur1=n>0?head1:head2;
        cur2=cur1==head1?head2:head1;
        n=Math.abs(n);
        while(n!=0){
            n--;
            cur1=cur1.next;
        }
        while(cur1!=cur2){
            cur1=cur1.next;
            cur2=cur2.next;
        }
        return cur1;
    }else{
        cur1=loop1.next;
        while(cur1!=loop1){
            if(cur1==loop2){
                return loop1;
            }
            cur1=cur1.next;
        }
        return null;
    }
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值