算法通关——链表高频面试题

1.两链表第一个公共子节点

1.1哈希集合

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

1.2栈

将两个链表的两个节点分别入两个栈,然后分别出栈,相等就继续出栈,最晚出栈的那一组即两个链表的第一个公共子节点。(花费2n(O)的空间复杂度)

Java的java.util.Stack类是java.util.Vector的子类,用于实现堆栈。下列常见操作

  1. push(E item): 把项压入栈顶。
  2. pop(): 移除栈顶项并作为此函数的值返回该项。
  3. peek(): 查看栈顶项而不移除它。
  4. empty(): 检查栈是否为空。
  5. search(Object o): 返回对象在堆栈中的位置,以 1 为基数。
  6. element(): 检索但不移除栈顶元素。
  7. elements(): 返回此堆栈中元素的迭代器。
  8. size(): 返回此集合中元素的数目。
  9. clear(): 移除此集合中的所有元素。
  10. equals(Object obj): 比较此对象与指定对象的等价性。
  11. hashCode(): 返回此对象的哈希码值。
import java.util.Stack;
public ListNode findFirstCommonNodeByStack(ListNode headA,ListNode headB){
    Stack<ListNode> stackA = new Stack();
    Stack<ListNode> stackB = new Stack();
    while(stackA != null){
        stackA.push(headA);
        headA = headA.next;
    }
    while(headB != null){
        stackB.push(headB);
        headB = headB.next;
    }

    ListNode preNode = null;

    while(stackA.size()>0&&stackB.size()>0){
        if(stackA.peek() == stackB.peek()){
            preNode = stackA.pop();
            stackB.pop();
        }else
        {
            break;
        }

    }
    return preNode;
}

2.判断链表是否为回文序列

2.1基础全部压栈法

public boolean isPalindrome(ListNode head){
    //拷贝原始链表并将其压栈
    ListNode temp = head;
    Stack<Integer> stack = new Stack();
    while(temp != null){
        stack.push(temp.value);
        temp = temp.next;
    }
    //将复制的链表出栈并与原始链表比对,出栈是倒叙,恰好满足回文序列的检测要求
    while(head != null){
        if(head.value != stack.pop()){
            return fasle;
        }
        //值相等则继续检查下一个节点。
        //此处pop()出栈操作,会自动移除栈顶元素,故已经压栈的拷贝链表无需考虑顺延至下一个节点
        head = head.next
    }
    return true;
}

tips:此基础操作有待优化

3.合并有序链表

3.1合并两个有序链表

例子:将两个升序链表合并为一个新的升序链表返回,新链表拼接两个链表所有节点组成。

思路:(1)新建一个链表,分别遍历两个原链表每次选最小节点接到新链表上。

           (2)将一个链表节点拆下,合并至另一个链表上。

若采用(1)代码如下

public ListNode mergeTwoLists(ListNode list1,ListNode list2){
    //新建链表头节点值为-1,不影响排序即可
    ListNode newHead = new ListNode(-1);
    //保留此新建链表头节点并命名为res,newHead更多的作为后续接入口使用
    ListNode res = newHead;
    while(list1 != null&&list2 != null){
        //两链表同长
        if(list1 != null&&list2 != null){
            if(list1.val < list2.val){
                newHead.next = list1;//list1原始表头节点接入newHead
                list1 = list1.next;//失去原表头的list1顺延获得新表头
            }else if(list1.val > list2.val){
                newHead.next = list2;
                list2 = list2.next; 
            }else if(list1.val == list2.val){
                newHead.next = list2;
                list2 = list2.next;
                newHead = newHead.next;//此处暂时更新newHead以便具有相等值的list1头节点也接入进来
                newHead.next = list1;
                list1 = list1.next;
            }newHead = newHead.next;//统一更新接入节点,供下次循环使用
        }
        //到此处本该结束,但考虑到两个要合并的链表有可能长短不一,有一个链表节点会先耗尽,剩下的链表仍要坚持完成接入任务
        else if(list1 != null && list2 == null)//余list1
        {
            newHead.next = list1;
            list1 = list1.next; 
            newHead = newHead.next;
        }
        else if(list2 != null && list1 == null)//余list2
        {
            newHead.next = list2;
            list2 = list2.next; 
            newHead = newHead.next;
        }
    }
    return res.next;//原来的那个值为-1的头节点就扔掉辣
}

写完发现有点臃肿,我们将一个while分解成三个比较好看,如下:

public ListNode mergeTwoLists(ListNode list1,ListNode list2){
    //新建链表头节点值为-1,不影响排序即可
    ListNode newHead = new ListNode(-1);
    //保留此新建链表头节点并命名为res,newHead更多的作为后续接入口使用
    ListNode res = newHead;
    while(list1 != null&&list2 != null){
        //两链表同长
        if(list1.val < list2.val){
            newHead.next = list1;//list1原始表头节点接入newHead
            list1 = list1.next;//失去原表头的list1顺延获得新表头
        }else if(list1.val > list2.val){
            newHead.next = list2;
            list2 = list2.next; 
        }else if(list1.val == list2.val){
            newHead.next = list2;
            list2 = list2.next;
            newHead = newHead.next;//此处暂时更新newHead以便具有相等值的list1头节点也接入进来
            newHead.next = list1;
            list1 = list1.next;
        }newHead = newHead.next;//统一更新接入节点,供下次循环使用        
    }    
    //到此处本该结束,但考虑到两个要合并的链表有可能长短不一,有一个链表节点会先耗尽,剩下的链表仍要坚持完成接入任务
    while(list1 != null && list2 == null)//余list1
    {
        newHead.next = list1;
        list1 = list1.next; 
        newHead = newHead.next;
    }
    while(list2 != null && list1 == null)//余list2
    {
        newHead.next = list2;
        list2 = list2.next; 
        newHead = newHead.next;
    }
   
    return res.next;//原来的那个值为-1的头节点就扔掉辣

}

细看,仍觉不优雅,继续优化直到最简

public ListNode mergeTwoLists(ListNode head1,ListNode head2){
    ListNode prehead = new ListNode(-1);
    ListNode prev = prehead;
    while(head1 != null && head2 != null){
    //等于的情况可合并至不等的一方
        if(head1.val <= head2.val){
            prev.next = list1;
            list1 = list1.next;
        }else{
            prev.next = list2;
            list2 = list2.next;
        }
        prev = prev.next;
    }
    //还剩一个未被合并完,剩下的接上即可,原链表都是升序的这里不会乱序
    prehead.next = list1 == null ? list2 : list1; 
    return prehead.next;
}

这里为化到最简使用了三目运算符——A ? B:C——若事件A为真则取B,不然取C。

3.2合并K个链表

先合并两个,剩下的再逐步与之合并

在mergeTwoLists的基础上构建函数 mergeKLists , 传参 (ListNodes[] lists)

public ListNode mergeKLists(ListNode[] lists){
    ListNode res = null;
    for(ListNode list: lists){
        mergeTwoLists(res,list);
    }
    return res; 
}

3.3链表の移花接木

链表list1,包含n个元素;链表list2,包含m个元素。请你将list1中下标 a 至 b 的节点删除(包括a,b),并在此接入list2。

class Solution {
    public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {

        ListNode preA = list1;
        for(int i=0;i < a-1;i++){
            preA = preA.next;
        }

        ListNode preB = preA;
  
        for(int i=0;i < b-a+2;i++){ 
            preB = preB.next;
        }
        //此处共删去b-a+1个节点,a-1 + b-a+1 + 1=b+1,preB即被切割的list1的后半段链表的头节点. 

        preA.next = list2;//list2头节点接list1前半部分的尾节点
        while(list2.next != null){
            list2 = list2.next;
        }
        list2.next = preB;//list2尾节点接list1后半部分的头节点
        return list1;
        
    }
}

4.双指针

4.1找中间节点

给单链表返回其中间节点(逢双则返回第二个中间节点)

我们使用快慢指针,slow一次走一步,fast一次走两步。fast到达末尾时Slow即中间节点。

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

 }

4.2寻找倒数第k个元素

public ListNode getKthFromEnd(ListNode head,int k){
    ListNode fast = head;
    ListNode slow = head;
    //本题从1开始计数,fast先走k步到第k+1位。然后和slow一起往后遍历,我们要fast走到末尾null节点,这时我们往前数k个数即找到倒数第k个节点,即slow.
    while(fast != null && k > 0){
        fast = fast.next;
        k--;
    }
    while(fast != null){
        fast = fast.next;
        slow = slow.next;
    }
    return slow;
}

4.3旋转链表

给出链表的头节点,旋转链表,将每个节点向右移动k个位置。

两种思路:

1.整个链表反转,再将前k和n-k两部分分别反转,反转链表。

2.双指针找到倒数k的位置,再将两链表拼接:

public ListNode rotateRight(ListNode head,int k){
    if(head == null || k == 0){
        return head;
    }
    ListNode slow = head;
    ListNode fast = head;
    ListNode temp = head;//记录原链表
    int len = 0;
    //先统计链表元素个数
    while(head != null){
        head = head.next;
        len++;
    }
    //向右移动k个单位,若k是len的整数倍,那么相当于没有旋转.取余而非除以是考虑到k>len
    if(k % len == 0){
        return temp;
    }
    while((k%len)>0){
        k--;
        fast = fast.next;
    }
    while(fast.next != null){
        fast = fast.next;
        slow = slow.next;
    }

    //此时fast为尾部节点,slow为要断开的地方,(此处指向右位移k位后的链表尾节点)
    ListNode res = slow.next;//确定头
    slow.next = null;//尾接null
    fast.next = temp;//中间拼接
    //以上三步是旋转后链表的拼接
    return res;    
    
}

5.链表删除专题

5.1删除特定节点

删除某特定值的节点

public ListNode removeElements(ListNode head,int val){
    ListNode dummyHead = new ListNode(0);
    dummyHead.next = head;
    ListNode cur = dummyHead;
    while(cur.next != null){
        if(cur.next.val == val){
            cur.next == cur.next.next;//跳过即可删除cur.next,cur还在的
        }else{
            cur = cur.next;//否则遍历
        }
    }
    return dummyHead.next;
} 

5.2删除倒数第N个节点

算链表长:

public ListNode removeNthFromEnd(ListNode head,int n){
    ListNode dummyHead = new ListNode(0);
    dummyHead.next = head;
    int length = getLength(head);
    ListNode cur = dummyHead;
    //找到倒数第n个节点
    for(int i;i<length-n+1;++i){
        cur = cur.next;
    }
    //找到就删掉
    cur.next = cur.next.next;
    return dummyHead.next;

    public int getLength(ListNode head){
        int length = 0;
        while(head != null){
            ++length;
            head = head.next;
        }
        return length;
    }
}

双指针:

public ListNode removeNthFromEnd(ListNode head,int n){
    ListNode dummyHead = new ListNode(0);
    dummyHead.next = head;
    ListNode first = head;
    ListNode second = dummyHead;
    for(int i=0; i<n;++i){
        first = first.next;
    }
    while(first != null){
        first = first.next;
        second = second.next;
    }
    second.next = second.next.next;
    return dummyHead.next;
}

5.3删除重复元素

保留一个重复元素/删除所有重复元素(假设给定链表排好序的)

若保留:

public ListNode deleteDuplicates(ListNode head){
    if(head == null){
        return head;
    }
    ListNode cur = head;
    while(cur.next != null){
        if(cur.val == cur.next.var){
            cur.next = cur.next.next;
        }else{
            cur = cur.next;
        }
    }
    return head;
}

若不保留:

public ListNode deleteDuplicates(ListNode head){
    if(head == null){
        return head;
    }
    ListNode dummy = new ListNode(0,head);//参数里的head说明dummy节点将被连接到head,无需 dummy.next = head  
    ListNode cur = dummy;//与保留一个重复元素的情况加以思考并区分
    while(cur.next != null &&cur.next.next != null){
        if(cur.next.val == cur.next.next.val){
            int x = cur.next.val;
            while(cur.next != null &&cur.next.val == x){
                cur.next = cur.next.next;
            }
        }
        else{
            cur = cur.next;
        }
    }
        return dummy.next;
}

6.回顾第一个公共子节点问题

使用栈或者集合都需要开辟  O(n)空间,我们尝试只用一两个变量解决.

6.1拼接两个字符串

public ListNode findFirstCommonNode(ListNode pHead1,ListNode pHead2){
    if(pHead1==null || pHead2==null){
        return null
    }
    ListNode p1=pHead1;
    ListNode p2=pHead2;
    while(p1!=p2){
        p1=p1.next;
        p2=p2.next;
        if(p1!=p2){//避免序列不存在交集时陷入死循环
            if(p1==null){
                p1=pHead2;
            }
            if(p2==null){
                p2=pHead1;
            }
        }

    }
    return p1;
}

6.2差和双指针

长的链表先走 一个链表长度差值,然后两个链表同时往前走,节点一样时即找到公共节点。

public ListNode findFirstCommonNode(ListNode pHead1,ListNode pHead2){
      if(pHead1==null || pHead2==null){
        return null
    }
    ListNode current1=pHead1;
    ListNode current2=pHead2;
    int l1=0,l2=0;
    while(current1!=null){
        current1=current1.next;
        l1++;
    }
     while(current2!=null){
        current2=current2.next;
        l2++;
    }

    current1=pHead1;
    current2=pHead2;
    int sub=l1>l2?l1-l2:l2-l1 ;
    if(l1>l2){
        int a=0;
        while(a<sub){
            current1=current1.next;
            a++;
        }
    }
    if(l1<l2){
        int a=0;
        while(a<sub){
            current2=current2.next;
            a++;
        }
    }
    while(current1!=current2){
        current1=current1.next;
        current2=current2.next;
    }

    return current1;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值