【算法竞赛-初级】基础数据结构-链表篇

	这个系列的文章主要是用来记录系统学习算法的心得思路,主要参考书籍是罗勇军老师的算法竞赛。
	现在目前目标是完成该书的初级部分,这篇文章是第一章基本数据结构中的链表部分。后续将持续更新该系列,欢迎大家一键三连!	

这里写目录标题

  • 第一章、基础数据结构
    • 1.1、链表(动态、静态、STL链表)
      • 1)从尾到头打印链表(LeetCode 剑指Offer 06)
      • 2)链表中倒数第k个节点(LeetCode 剑指Offer 22)
      • 3)反转链表(LeetCode 剑指Offer 24)
      • 4)合并两个有序链表(LeetCode 21)
      • 5)复杂链表的复制(LeetCode 剑指Offer 35)
      • 6)两个链表的第一个公共节点(LeetCode 剑指Offer 52)
      • 7)删除链表中的节点(LeetCode 237)

第一章、基础数据结构

1.1、链表(动态、静态、STL链表)

题目:从尾到头打印链表、链表中倒数第k个节点、反转链表、合并两个有序链表、复杂链表的复制、两个链表的第一个公共节点、删除链表中的节点

1)从尾到头打印链表(LeetCode 剑指Offer 06)

1、思想:首先,head为null的话,肯定是没有元素,直接返回new int[]{};其次,new一个栈Stack stack = new Stack<>(),遍历链表,通过当前链表的大小(有几个元素),并将元素存入栈中;最后,新建一个大小为栈大小的int[]数组,通过栈(先进后出)中的元素逐个弹出,即可完成我们的要求,在这个过程中,时间复杂度为O(n)、空间复杂度也有O(n);

2、改进:因为从尾到头,就是反过来,所以我们可以第一次遍历的时候统计好数组的大小,然后不借助栈,直接让结果数组下标从size-1开始存,这样也能实现链表的反转。

class Solution {
    public int[] reversePrint(ListNode head) {
        if(head == null){
            return new int[]{};
        }
        int sum = 0;
        Stack<Integer> stack = new Stack<Integer>();
        ListNode cur = head;
        while(cur != null){
            sum++;
            stack.push(cur.val);
            cur = cur.next;
        }
        int[] res = new int[sum];
        sum = 0;
        while(!stack.empty()){
            res[sum] = stack.pop();
            sum++;
        }
        return res;
    }
}

2)链表中倒数第k个节点(LeetCode 剑指Offer 22)

0、知识点:双指针

1、思想:很巧妙的双指针思想,我们定义两个指针,first和second,让first先走k步,再让first和second同时走(直到first为null),此时second指针所指向的位置就是倒数第k个节点的位置,因为只做一次遍历,所以时间复杂度为O(n),空间复杂度为O(1),只有两个指针和一个步数的变量count。

2、改进:这算是一种挺好的解决办法,在LeetCode上,能做到时间和内存打败:100%和91%。

class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        // 很巧妙的双指针思想:让指针1走k步,再让指针2和指针1同时走(直到指针1指向null)
        int count = 0;
        ListNode first = head, second = head;
        while(count < k){
            first = first.next;
            count++;
        }
        while(first != null){
            first = first.next;
            second = second.next;
        }
        return second;

    }
}

3)反转链表(LeetCode 剑指Offer 24)

0、知识点:原地置换(创新点)

1、思想:第一个想到的就是头插法,即通过维护两个指针pre和cur,不断的将cur经过的节点断开,再将其通过头插法插回原数组中,使用虚拟头节点(ListNode zero = new ListNode(-1))可以减少对头节点的处理,其中需要注意的是pre和cur的更新策略,我自己的做法,pre只需要在第一次移动的时候跟着移动,这样pre就能始终保持在我们要断开节点的前一位,while循环里做的步骤(断开节点、取出断开节点、更新cur、头插断开节点、更新pre(只需一次)),时间复杂度为O(n),空间复杂度为O(1),但是这个算法实现的话,时间上是没问题的可以击败100%,空间上的话只有击败30%。

2、改进:原地置换,简单来说就是改变指针的指向,同样的有pre和cur指向,初始:cur指向head,而pre指向null,然后cur指向指向pre,pre暂存cur值,不断的迭代就可以实现指针的转向,这个地方有很详细的图文讲解:原地置换详解。

class Solution {
    public ListNode reverseList(ListNode head) {
        // 反转链表的话可以考虑用头插法来解决,头插法的思想就是先插的排在末尾
        // 所以我们只要将链表元素逐个断开,再这个头插进来即可
        // 边界判断
        if(head == null) return null;
        // 创建虚拟头节点
        ListNode zero = new ListNode(-1);
        zero.next = head;
        // 定义双指针
        ListNode pre = zero, cur = head;
        // 判断是否是第一次更新
        boolean flag = false;
        while(cur != null){
            // 断开节点
            pre.next = cur.next;
            // 取出断开节点
            ListNode temp = cur;
            // 更新cur
            cur = pre.next;
            // 头插temp
            temp.next = zero.next;
            zero.next = temp;   
            // 更新pre: 只需要在第一次的时候更新节课
            if(flag == false){
                pre = pre.next;
                flag = true;
            }
            
        }
        // pre.next = null;
        return zero.next;
    }
}

4)合并两个有序链表(LeetCode 21)

1、思想:同时对两个链表进行遍历,直到有一方遍历指针指向null,则结束循环,将剩下的链表的值接到原来的链表中。可以通过利用原来的某个链表,不过需要额外的一个指针,链表这样注意处理的过程中节点之间的依赖性,逻辑上需要多注意!这样的思路可以做到时空复杂度为:O(n+m) 、O(1)的一个组合。

2、改进:递归(高阶解法)

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode zero = new ListNode(-1);
        zero.next = list1;
        ListNode pre = zero;
        ListNode cur1 = list1, cur2 = list2;
        while(cur1 != null && cur2 != null){
            if(cur1.val > cur2.val){
                ListNode temp = cur2;
                cur2 = cur2.next;
                temp.next = pre.next;
                pre.next = temp;
                pre = temp;
            }else{
                pre = cur1;
                cur1 = cur1.next;
            }
        }
        pre.next = cur1 == null ? cur2 : cur1;
        return zero.next;
    }
}

5)复杂链表的复制(LeetCode 剑指Offer 35)

0、知识点:深拷贝

1、思想:这个问题考虑的地方在于如果是简单链表的话我们可以直接创建链表进行复制,但是由于random的指针指向的地方有可能是还没创建的节点,所以这个地方用简单链表的思想是实现不通的。这里我们选择利用哈希表,构建原有节点到新节点的映射,然后再对新节点的next和random进行连接,为了方便理解,这里贴出代码.时空复杂度为:O(n)、O(1)

class Solution {

    Map<Node, Node> cacheMap = new HashMap<Node, Node>();

    public Node copyRandomList(Node head) {
        if(head == null){
            return null;
        }
        Node cur = head;
        while(cur != null){
            if(!cacheMap.containsKey(cur)){
                // 相当于说复制了各个节点,并建立“原来节点-新节点”的Map映射
                cacheMap.put(cur, new Node(cur.val));
            }
            cur = cur.next;
        }
        cur = head;
        // 构建链表的next和random指向
        while(cur != null){
          	// 这里需要注意,我们不是直接用cur.next、cur.random
          	// 而是利用我们cacheMap中原有节点和新节点之间的映射,来对cacaheMap新节点(第二个泛型)的next和random进行完善补充!
            cacheMap.get(cur).next = cacheMap.get(cur.next);
            cacheMap.get(cur).random = cacheMap.get(cur.random);
            cur = cur.next;
        }
        return cacheMap.get(head);
    }
}

6)两个链表的第一个公共节点(LeetCode 剑指Offer 52)

1、思路:在第一次做的时候呢,我也没有想到是要怎么来实现,我们来假设看看,

  • 假设链表a和链表b是有公共节点的:
    • 情况1(a和b等长),那么意味着两个指针分别从a和b出发,肯定是会一起到达他们的第一个公共节点;
    • 情况2(a和b不等长),因为公共点存在(两条链表会从公共点后合并成一条链表),所以我们逆向思维,从合并后的末尾出发,是不是两个指针走的步数是一样的,但是!我们现在存在距离差,a和b的起跑线不同,所以我们要去消除这个区别,刚刚的解释就尤为重要,他们距离结尾的距离是一样的,所以我们只要让指针cur1和cur2同时从a和b的head出发,走到null的时候从另外一条链表的头节点出发,这样就可以保证他们走到最后的距离都是a+b,且公共节点对cur1和cur2在a+b的距离上,它所处的距离是等价的,这样我们就可以轻松找到第一个公共点!
    • 时空复杂度:O(n)和O(1)

2、改进:️(注意事项:在写判断逻辑的时候要注意,先判断两者是否相等,再判断是否为null,决定是要cur = cur.next,还是cur1 = headB,最后在再写循环结束条件:cur1和cur2都指向null)

class Solution {
    ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode cur1 = headA, cur2 = headB;
        // 只要两个指针有一个不为null,按照我们的思路,如果他们走完a+b还没找到的话,那么就是失败的
        // 而且只能一次交换遍历链表
        while(cur1 != null || cur2 != null){
            if(cur1 == cur2){
                return cur1;
            }
            if(cur1 == null){
                cur1 = headB;
            }else{
                cur1 = cur1.next;
            }
            if(cur2 == null){
                cur2 = headA;
            }else{
                cur2 = cur2.next;
            }
            if(cur1 == null && cur2 == null){
                return null;
            }
        }
        // 没找到
        return null;
    }
}

7)删除链表中的节点(LeetCode 237)

1、思路:这题是很有趣的一个问题,我们要删除这个节点,按照正常的思路就是知道这个节点的前一个节点,让前一个节点来进行删除,但是由于题目没有给出链表,所以我们灵机一动,我们只要把后一个节点的值复制给node,这样相当于要删除的节点就是node.next,我们就可以利用node来做删除操作。时空复杂度分别为:O(1)、O(1)

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public void deleteNode(ListNode node) {
        // 知道现在的node位置,我们让下一个位置的val赋值给node,再把node的下一个节点删除
        node.val = node.next.val;
        node.next = node.next.next;

    }
}

你可能感兴趣的:(算法,java,链表,数据结构,算法)