Leetcode(力扣)超高频题讲解(三)

高频题(三)

目录:

Leetcode(力扣)超高频题讲解(一)

Leetcode(力扣)超高频题讲解(二)

一、螺旋矩阵II(59)

题目描述:

给你一个正整数 n ,生成一个包含 1n^2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix,如下图所示:

image-20210829210826388

输入:3

输出:[[1, 2, 3], [8, 9, 4], [7, 6, 5]]

思路:

根据给定的参数值 n,生成一个 n*n 大小的空矩阵,随后从第一个元素[0][0]开始,按照从左往右、从上往下、从右往左、从下往上的顺序模拟螺旋过程。

定义上下左右的边界,t,b,l,r,向矩阵中存放的值 num 的初始值为1,最终值 tar 为 n*n,以 num <= tar 为条件,按照上述的过程遍历矩阵。

每填入一个数字,就将num值加1;每填满一行或一列,就将对应的边界向内缩1,比如从左到右填完一行后,上边界 t+1,相当于上边界向内缩 1。

具体过程如下图所示:

image-20210601225754877

代码实现:

class Solution {
    
    
    public int[][] generateMatrix(int n) {
    
    

        //左边界和上边界的初始值为0,缩小即加1
        //右边界和下边界的初始值为n-1,缩小即减1
        int l = 0, r = n - 1, t = 0, b = n - 1;

        //创建一个二维数组作为最终的返回值
        int[][] mat = new int[n][n];

        //定义放入矩阵的初始值和最终值
        int num = 1, tar = n * n;

        //放入数据至矩阵
        while(num <= tar){
    
    

            //从左向右填充
            for(int i = l; i <= r; i++) {
    
    
                mat[t][i] = num++; 
            }
            t++; //上边界缩小,加1

            //从上向下填充
            for(int i = t; i <= b; i++) {
    
    
                mat[i][r] = num++; 
            }
            r--;

            //从右向左填充
            for(int i = r; i >= l; i--) {
    
    
                mat[b][i] = num++;
            }
            b--;

            //从下向上填充
            for(int i = b; i >= t; i--) {
    
    
                mat[i][l] = num++;
            }
            l++;
        }
        return mat;
    }
}
image-20210601230009234

二、螺旋矩阵(54)

题目描述:

给你一个 mn 列的矩阵 matrix ,请按照顺时针螺旋顺序 ,返回矩阵中的所有元素,如下图所示:

image-20210829222614776 image-20210829222627054

思路:

“螺旋矩阵II” 思路一致,不再赘述。

注意:循环条件是:求出矩阵中元素个数 numEle(行数 * 列数),每遍历一个位置,就将 numEle - 1,故条件为 while(numEle >= 1)

代码实现:

private static List<Integer> spiralOrder(int[][] matrix) {
    
    
    
    LinkedList<Integer> result = new LinkedList<>();
    
    //特殊条件判断
    if(matrix==null||matrix.length==0) return result;
    
    //定义边界
    int left = 0;
    int right = matrix[0].length - 1;
    int top = 0;
    int bottom = matrix.length - 1;
    
    //矩阵中元素的个数
    int numEle = matrix.length * matrix[0].length;
    
    while (1 <= numEle) {
    
    
        
        //为什么for循环中也要判断numEle >= 1?
        //如果这是一个长为10,宽为3的长方形,当遍历完最外层的一圈之后,又回到第二层从左向右遍历 (每进入一次while循环,执行最开始的),当遍历结束之后,numEle的值减小为0,由于已经进入了while循环,所以还会继续向下执行,当执行到从右向左时,由于left的值一定是小于right的,如果不判断numEle >= 1?,那么就会遍历出重复的元素。
        
        //左右
        for (int i = left; i <= right && numEle >= 1; i++) {
    
    
            result.add(matrix[top][i]);
            numEle--; //一定不要忘记个数要减一
        }
        top++;
        
        //上下
        for (int i = top; i <= bottom && numEle >= 1; i++) {
    
    
            result.add(matrix[i][right]);
            numEle--;
        }
        right--;
        
        //右左
        for (int i = right; i >= left && numEle >= 1; i--) {
    
    
            result.add(matrix[bottom][i]);
            numEle--;
        }
        bottom--;
        
        //下上
        for (int i = bottom; i >= top && numEle >= 1; i--) {
    
    
            result.add(matrix[i][left]);
            numEle--;
        }
        left++;
    }
    return result;
}
image-20210601231207531

三、岛屿的最大面积(695)

题目描述:

给定一个包含了一些 0 和 1 的非空二维数组 grid,一个岛屿是一组相邻的 1(代表陆地),这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表海洋)包围着。

找到给定的二维数组中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。

前置知识:

网格问题的基本概念:

网格问题是由 m×n 个小方格组成一个网格,每个小方格与其上下左右四个方格认为是相邻的,要在这样的网格上进行某种搜索。

岛屿问题是一类典型的网格问题。每个格子中的数字可能是 0 或者 1。把数字为 0 的格子看成海洋格子,数字为 1 的格子看成陆地格子,这样相邻的陆地格子就连接成一个岛屿,如下图所示:

image-20210608204815741

二叉树的DFS:

注意:二叉树的前中后序遍历都是DFS,层序遍历是BFS。

void traverse(TreeNode root) {
    
    
    // 判断 base case
    if (root == null) {
    
    
        return;
    }
    // 访问两个相邻结点:左子结点、右子结点
    traverse(root.left);
    traverse(root.right);
}

可以看到,二叉树的 DFS 有两个要素:「访问相邻结点」和「判断 base case」。

  • 第一个要素是 访问相邻结点。二叉树的相邻结点非常简单,只有左子结点和右子结点两个。二叉树本身就是一个递归定义的结构:一棵二叉树,它的左子树和右子树也是一棵二叉树。那么 DFS 遍历只需要递归调用左子树和右子树即可。

  • 第二个要素是 判断 base case。一般来说,二叉树遍历的 base case 是 root == null。这样一个条件判断其实有两个含义:一方面,这表示 root 指向的子树为空,不需要再往下遍历了。另一方面,在 root == null 的时候及时返回,可以让后面的 root.left 和 root.right 操作不会出现空指针异常。

网格问题的DFS:

网格DFS的两个要素:

  • 相邻节点

    • 对于格子 (r, c) 来说(r 和 c 分别代表行坐标和列坐标),四个相邻的格子分别是 (r-1, c)、(r+1, c)、(r, c-1)、(r, c+1),也就是上下左右四个格子,换句话说,网格结构是「四叉」的,如下图所示:

      image-20210608210804407
  • base case

    • 从二叉树的 base case 对应过来,网格的 base case 是网格中不需要继续遍历的格子, 也就是 grid[r][c] 中会超出网格范围的格子,如下图所示:

      image-20210608210850309

有了两个要素之后,可以得到网格DFS遍历的代码:

void dfs(int[][] grid, int r, int c) {
    
    
    
    // 判断 base case
    // 如果坐标 (r, c) 超出了网格范围,直接返回
    if (!inArea(grid, r, c)) {
    
    
        return;
    }
    
    // 访问上、下、左、右四个相邻结点
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
    
    
    return 0 <= r && r < grid.length 
        && 0 <= c && c < grid[0].length;
}

如何避免重复:

每走过一个陆地格子,就把格子的值改为 2,这样当遇到 2 的时候,就知道这是遍历过的格子了。也就是说,每个格子可能取三个值:

  • 0 —— 海洋格子
  • 1 —— 陆地格子(未遍历过)
  • 2 —— 陆地格子(已遍历过)

最终的网格DFS遍历的代码:

void dfs(int[][] grid, int r, int c) {
    
    
    
    // 判断 base case
    if (!inArea(grid, r, c)) {
    
    
        return;
    }
    
    // 如果这个格子被遍历过,直接返回
    if (grid[r][c] != 1) {
    
    
        return;
    }
    
    grid[r][c] = 2; // 将格子标记为「已遍历过」

    // 访问上、下、左、右四个相邻结点
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
    
    
    return 0 <= r && r < grid.length 
        && 0 <= c && c < grid[0].length;
}

具体的过程如下图所示:

image-20210608211549939

注意:这段代码仅表示从某一个节点开始遍历他相邻的所有陆地节点,如果要遍历整个二维数组的所有陆地节点,需要对每个陆地节点都进行上述的递归过程。

本题思路:

只需要对每个岛屿做 DFS 遍历,求出每个岛屿的面积就可以了。求岛屿面积的方法也很简单,每遍历到一个格子,就把面积加一。

代码实现:

public int maxAreaOfIsland(int[][] grid) {
    
    

    //最终结果值
    int res = 0;

    //遍历二维数组的所有节点,只有是陆地节点才进行递归
    for (int r = 0; r < grid.length; r++) {
    
    
        for (int c = 0; c < grid[0].length; c++) {
    
    
            
            //每判断得到一个没有被遍历过的格子,就会把它所有相邻陆地格子全部遍历
            if (grid[r][c] == 1) {
    
    
                int a = area(grid, r, c);
                res = Math.max(res, a);
            }
        }
    }
    return res;
}

//从某一个节点开始遍历他所相邻的陆地节点
int area(int[][] grid, int r, int c) {
    
    
    
    //两种结束条件:1.越界 2.此节点被遍历过或者是海洋节点
    if (!inArea(grid, r, c) || grid[r][c] != 1) {
    
    
        return 0;
    }

    //遍历过的节点进行标记,防止重复遍历
    grid[r][c] = 2;

    return 1 
        + area(grid, r - 1, c)
        + area(grid, r + 1, c)
        + area(grid, r, c - 1)
        + area(grid, r, c + 1);
}

//判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
    
    
    return 0 <= r && r < grid.length 
        && 0 <= c && c < grid[0].length;
}
image-20210608215206989

四、岛屿数量(200)

题目描述:

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

思路:

根据 “岛屿的最大面积” 题目的DFS思路,很容易得出此题的解题思路,每遍历一个岛屿将结果值加一即可。

代码实现:

class Solution {
    
    
    public int numIslands(char[][] grid) {
    
    
        int res = 0;
        for (int i = 0; i < grid.length; i++) {
    
    
            for (int j = 0; j < grid[0].length; j++) {
    
    
                if (grid[i][j] == '1') {
    
     //注意比较的是字符
                    countLands(grid, i, j);
                    res++;
                }
            }
        }
        return res;
    }

    public void countLands(char[][] arr, int l, int r) {
    
    
        if (!isInGrid(arr, l, r) || arr[l][r] != '1') return;
        arr[l][r] = '2';
        countLands(arr, l, r + 1);
        countLands(arr, l, r - 1);
        countLands(arr, l + 1, r);
        countLands(arr, l - 1, r);
    }

    public boolean isInGrid(char[][] arr, int l, int r) {
    
    
        return l >= 0 && l < arr.length && r >= 0 && r < arr[0].length;
    }
}
image-20210608215223801

五、反转链表II(92)

题目描述:

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

image-20210829230937296

输入 head = 1,left = 2,right = 4;输出[1, 4, 3, 2, 5]。

注意:输入的 left 和 right 并不是索引的位置,而是节点的取值。

思路:

使用两个指针 g 和 p ,g 指向上图的1(left的前一个位置),p 指向上图的2(left的位置),让 p 后面的一个节点移动到 g 的后面(先删除后添加),重复上述操作 right - left 次,即可翻转。

代码实现:

class Solution {
    
    
    public ListNode reverseBetween(ListNode head, int m, int n) {
    
    
        
        //定义一个辅助头节点
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;

        //初始化指针
        //初始g指向辅助头节点
        //初始p指向头节点
        ListNode g = dummyHead;
        ListNode p = head; 

        //将指针移到相应的位置
        //注意给的头节点初始值是1不是0
        for(int step = 0; step < m - 1; step++) {
    
    
            g = g.next; p = p.next;
        }

        // 头插法插入节点
        for (int i = 0; i < n - m; i++) {
    
    
            ListNode removed = p.next;
            
            p.next = p.next.next; //删除p之后的节点
            
            //插入到g的后面
            removed.next = g.next;
            g.next = removed;
        }

        return dummyHead.next;
    }
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

六、用栈实现队列(232)

思路:

使用两个栈push和pop,添加元素时,将元素添加至push栈中;取出元素时,将push栈中的所有元素移至pop栈,再取出pop栈栈顶的元素;查看队列第一个元素时,与取出元素类似,只不过仅查看pop栈栈顶元素但不弹出。

注意:

(1) push栈中的元素移至pop栈时,必须一次性全部移到pop中

(2) pop栈中有元素时,push栈中的元素一定不能移到pop中

代码实现:

class MyQueue {
    
    

    Stack<Integer> stackPush;
    Stack<Integer> stackPop;

    /** Initialize your data structure here. */
    public MyQueue() {
    
    
        stackPush = new Stack<>();
        stackPop = new Stack<>();
    }

    /** Push element x to the back of queue. */
    public void push(int x) {
    
    
        stackPush.push(x);
    }

    /** Removes the element from in front of queue and returns that element. */
    public int pop() {
    
    
        if(stackPush.empty() && stackPop.empty()) {
    
    
            throw new RuntimeException("队列为空!");
        }
        if(stackPop.empty()) {
    
    
            while (!stackPush.empty()) {
    
    
                stackPop.push(stackPush.pop());
            }
        }
        return stackPop.pop();
    }

    /** Get the front element. */
    public int peek() {
    
    
        if(stackPush.empty() && stackPop.empty()) {
    
    
            throw new RuntimeException("队列为空!");
        }
        if(stackPop.empty()) {
    
    
            while (!stackPush.empty()) {
    
    
                stackPop.push(stackPush.pop());
            }
        }
        return stackPop.peek();
    }

    /** Returns whether the queue is empty. */
    public boolean empty() {
    
    
        if(stackPush.empty() && stackPop.empty()) {
    
    
            return true;
        }
        return false;
    }
}
image-20210507100558792

七、用队列实现栈(225)

1. 两个队列

思路:

使用两个队列 queue1queue2queue1 队列用来存放栈中的所有元素,queue2 队列用来辅助元素入栈,每当有元素入栈时,先加入 queue2 (此时 queue2 中没有元素),然后再将 queue1 中的所有元素弹出后加入到 queue2 ,此时新加入的元素成为 queue2 的最后一个元素(会被第一个弹出),然后再将 queue1queue2 的内容交换(保证 queue1 中的元素是此时栈中的所有元素),同时也保证每次有新元素加入到 queue2queue2 是空的(以保证新加入的元素一定是第一个要弹出的)。弹出元素时,弹出 queue1 的第一个元素即可。

注意:

  1. 队列的初始化使用 new LinkedList()
  2. swap() 交换方法不适用类类型的地址值交换,方法执行完会出栈,地址值最终没有发生改变。(所以此题实际使用了3个队列,其中一个作为交换时的temp)

代码实现:

class MyStack {
    
    
    Queue<Integer> queue1;
    Queue<Integer> queue2;

    public MyStack() {
    
    
        queue1 = new LinkedList<Integer>();
        queue2 = new LinkedList<Integer>();
    }
    
    public void push(int x) {
    
    
        queue2.offer(x);
        while (!queue1.isEmpty()) {
    
    
            queue2.offer(queue1.poll());
        }
        
        //交换两个队列的内容,不能另写一个swap方法传递两个队列的引用值,会导致交换失败
        Queue<Integer> temp = queue1;
        queue1 = queue2;
        queue2 = temp;
    }
    
    public int pop() {
    
    
        return queue1.poll();
    }
    
    public int top() {
    
    
        return queue1.peek();
    }
    
    public boolean empty() {
    
    
        return queue1.isEmpty();
    }
}
image-20220115200153395

2. 一个队列

思路:

新加入元素时,先获得当前队列元素的个数,比如为n,然后将此元素加入队列(此时新加入的元素会被最后弹出),然后将本队列的之前的n个元素全部弹出后重新加入队列中,即可保证新加入的元素会被最先弹出。

代码实现:

class MyStack {
    
    
    Queue<Integer> queue;

    public MyStack() {
    
    
        queue = new LinkedList<Integer>();
    }

    public void push(int x) {
    
    
        int n = queue.size();
        queue.offer(x);
        for (int i = 0; i < n; i++) {
    
    
            queue.offer(queue.poll());
        }
    }

    public int pop() {
    
    
        return queue.poll();
    }

    public int top() {
    
    
        return queue.peek();
    }

    public boolean empty() {
    
    
        return queue.isEmpty();
    }
}
image-20220115203915177

八、二叉树的右视图(199)

题目描述:

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值,如下图所示:

image-20210829223634909 image-20210829223506592

1. BFS

思路:

与二叉树的层序遍历思路类似,每次仅保存每一层节点的最后一个元素。

注意:向队列中添加的时候是按照左→右孩子节点的顺序,所以每层弹出的第一个节点是如上图的2或5(每一层的最左边节点),故应当保留每一层的最后一个元素。

如何判断某个元素是某一层的最后一个元素呢?当要弹出节点时,会先统计当前层的元素个数,假设为 size ,通过 for (int i = 0; i < size; i++) 弹出元素时,如果 i == size - 1 则说明此元素是当前层的最后一个元素。

代码实现:

class Solution {
    
    
    public List<Integer> rightSideView(TreeNode root) {
    
    

        List<Integer> res = new ArrayList<>();
        if (root == null) {
    
    
            return res;
        }

        //队列
        ArrayDeque<TreeNode> arrayDeque = new ArrayDeque<>();
        arrayDeque.addFirst(root);

        while (!arrayDeque.isEmpty()) {
    
    

            //统计某一层的元素个数
            int size = arrayDeque.size();

            for (int i = 0; i < size; i++) {
    
    
                TreeNode node = arrayDeque.pollLast();

                //保留每一层的最后一个元素
                if (i == size - 1) {
    
    
                    res.add(node.val);
                }

                if (node.left != null) {
    
    
                    arrayDeque.addFirst(node.left);
                }
                if (node.right != null) {
    
    
                    arrayDeque.addFirst(node.right);
                }
            }
        }
        return res;
    }
}
image-20210806223703398

2. DFS

思路:

按照 「根结点 -> 右子树 -> 左子树」 的顺序访问,就可以保证每层都是最先访问最右边的节点,如下图:

image-20210806225703880

最终的结果为:1、3、4、9、8

注意:

每一层中只有一个节点会添加到结果集中,所以如果当前节点所在的层数(根节点的层数为0)等于结果集中节点的个数,表示这个节点是当前层第一个遍历到的节点。

代码实现:

class Solution {
    
    
    List<Integer> res = new ArrayList<>();

    public List<Integer> rightSideView(TreeNode root) {
    
    
        dfs(root, 0); // 从根节点开始访问,根节点深度是0
        return res;
    }

    private void dfs(TreeNode root, int depth) {
    
    
        if (root == null) {
    
    
            return;
        }
        
        //当前节点是所在层第一个被访问的节点(一定是最右边的节点),因此将当前节点加入res中
        if (depth == res.size()) {
    
       
            res.add(root.val);
        }
        
        //遍历完当前层的节点后,深度增加,表示该遍历子节点
        depth++;
        
        //注意:递归过程中定义的变量可能会随着递归的返回,值逐渐变小
        
        dfs(root.right, depth);
        dfs(root.left, depth);
    }
}

image-20210806225558930

九、链表中倒数第K个节点(剑指Offer22)

题目描述:

输入一个链表,输出该链表中倒数第k个节点。

思路:

使用快慢指针,快指针fast,慢指针slow。初始时二者都指向头节点,先让fast向后移动k步。之后二者一起向后移动(fastslow 都是每次向后移动一个节点),直到fast指向null,那么此时slow指向的就是倒数第k个节点,如下图所示:

image-20210910215740134 image-20210910215841712 image-20210910215937898

上述思路的可行性:先让快慢指针相距 k 个节点,然后一起移动,当快指针指向 null 的时候,由于快慢指针相距 k,所以慢指针一定指向倒数第 k 个节点。

代码实现:

class Solution {
    
    
    public ListNode getKthFromEnd(ListNode head, int k) {
    
    
        if (head == null) return null;

        ListNode fast = head;
        ListNode slow = head;

        //快指针向后移动k步
        for(int i = 0; i < k; i++) {
    
    
            //一定是先判断fast是否为null
            if (fast == null) return null;
            fast = fast.next;
        }

        //两个指针一起向后移动
        while (fast != null) {
    
    
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }
}
  • 时间复杂度 O(N) : N 为链表长度。总体看, fast 走了 N 步, slow 走了 (N-k) 步。
  • 空间复杂度 O(1) : 双指针 fast , slow 使用常数大小的额外空间。

注意:

for(int i = 0; i < k; i++) {
    
    
    //一定是先判断fast是否为null
    if (fast == null) return null;
    fast = fast.next;
}

如果链表为[1],且参数k的值为1,那么应该返回1,如果使用如下代码:

for(int i = 0; i < k; i++) {
    
    
    fast = fast.next;
    if (fast == null) return null;
}

结果返回的是null,出错。

所以一定是先判断fast是否为null,执行完第一次循环之后,第二次循环无法满足 i < k 条件,不会进入for循环,所以不会返回null。(先判断,后移动)

十、链表的中间节点(876)

题目描述:

给定一个头结点为 head 的非空单链表,返回链表的中间结点。

1. 奇数返回中点,偶数返回上中点

如果链表节点的个数为奇数,则返回中点;

如果链表节点的个数为偶数,则返回上中点(中点的前一个)

代码实现:

public static Node midOrUpMidNode(Node head) {
    
    
    
    // 链表为空返回null,有一个节点或两个节点均返回头节点
    if (head == null || head.next == null || head.next.next == null) {
    
    
        return head;
    }	
    
    // 执行到此表示链表至少有三个节点
    Node slow = head.next; //慢指针初始指向第二个节点
    Node fast = head.next.next; //快指针初始指向第三个结点
    
    // 循环条件要保证快指针不会越界
    while (fast.next != null && fast.next.next != null) {
    
    
        slow = slow.next; //慢指针一次走一步
        fast = fast.next.next; //快指针一次走两步
    }
    return slow;
}

解题思路:

使用两个指针,快指针每次移动两步,慢指针每次移动一步,考虑前5个节点即可得到快慢指针的初始位置:

  • 链表没有节点:返回头节点(null)
  • 只有1个节点:返回头节点
  • 只有2个节点:返回头节点
  • 只有3个节点:返回第二个节点,不能返回头节点,所以返回头节点的情况已经判断结束初始需要将慢指针移动到第二个节点,返回慢指针即可。
  • 只有4个节点:返回第二个节点,即慢指针没有移动,也就是说不满足条件 fast.next != null && fast.next.next != null
  • 只有5个节点:返回第三个节点,慢指针向后移动了一位,即满足了条件 fast.next != null && fast.next.next != null ,可以推得快指针初始位置在第3个节点,才可以保证快指针能移动到第5个节点。

2. 奇数返回中点,偶数返回下中点

public static Node midOrDownMidNode(Node head) {
    
    
    // 没有节点返回null,有一个节点返回头节点
    if (head == null || head.next == null) {
    
    
        return head;
    }
    Node slow = head.next;
    Node fast = head.next;
    while (fast.next != null && fast.next.next != null) {
    
    
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}

3. 奇数返回中点的前一个,偶数返回上中点的前一个

public static Node midOrUpMidPreNode(Node head) {
    
    
    // 空链表、一个节点、两个节点均返回null
    if (head == null || head.next == null || head.next.next == null) {
    
    
        return null;
    }
    Node slow = head; //指向第一个节点
    Node fast = head.next.next; //指向第三个节点
    while (fast.next != null && fast.next.next != null) {
    
    
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}

4. 奇数返回中点的前一个,偶数返回下中点的前一个

public static Node midOrDownMidPreNode(Node head) {
    
    
    if (head == null || head.next == null) {
    
    
        return head;
    }
    Node slow = head;
    Node fast = head.next;
    while (fast.next != null && fast.next.next != null) {
    
    
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}

上述四种情况的复杂度:

  • 时间复杂度:O(N),其中 N 是给定链表的结点数目。
  • 空间复杂度:O(1),只需要常数空间存放 slowfast 两个指针。

十一、重排链表(143)

题目描述:

给定一个单链表 L 的头节点 head ,单链表 L 表示为:

L0 → L1 → … → Ln-1 → Ln

请将其重新排列后变为:

L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → …(第一个节点后面是倒数第一个节点,第二个节点后面是倒数第二个节点…)

如下图所示:

image-20210911010415594

思路:

注意到目标链表即为将原链表的左半段和反转后的右半段合并后的结果。

可划分为三步:

  1. 找到原链表的中点

    注意:奇数返回中点,偶数返回上中点

  2. 将原链表的右半段反转

  3. 将左半段和反转后的右半段合并

代码实现:

class Solution {
    
    
    public void reorderList(ListNode head) {
    
    
        
        //特殊值判断
        if (head == null) {
    
    
            return;
        }

        //得到链表中点
        ListNode mid = middleNode(head);

        //定义两段链表的起始节点
        ListNode l1 = head; //左半段
        ListNode l2 = mid.next; //右半段

        //仅保留原链表的左半段,用来合并
        mid.next = null;

        //反转右半段
        l2 = reverseList(l2);

        //合并
        mergeList(l1, l2);
    }

    //得到链表中点
    public ListNode middleNode(ListNode head) {
    
    
        if (head.next == null || head.next.next == null) {
    
    
            return head;
        }	

        //慢指针初始指向第二个节点
        ListNode slow = head.next;
        //快指针初始指向第三个结点
        ListNode fast = head.next.next; 

        // 循环条件要保证快指针不会越界
        while (fast.next != null && fast.next.next != null) {
    
    
            slow = slow.next; 
            fast = fast.next.next; 
        }
        return slow;
    }

    //反转右半段
    public ListNode reverseList(ListNode head) {
    
    
        if (node == null || node.next == null) {
    
    
            return node;
        }
        ListNode res = reverseList(node.next);
        node.next.next = node;
        node.next = null;
        return res;
    }

    //合并
    public void mergeList(ListNode l1, ListNode l2) {
    
    
        
        //两个辅助结点用来保留两段链表中的某一个节点的后一个节点,防止断开
        ListNode l1_tmp;
        ListNode l2_tmp;
        
        while (l1 != null && l2 != null) {
    
    
            
            l1_tmp = l1.next;
            l2_tmp = l2.next;

            //开始真正的合并操作
            l1.next = l2;
            
            //让l1移动到原链表的下一个节点
            l1 = l1_tmp;

            l2.next = l1;
            l2 = l2_tmp;
        }
    }
}
  • 时间复杂度:O(N),其中 N 是链表中的节点数。
  • 空间复杂度:O(1)。

猜你喜欢

转载自blog.csdn.net/weixin_49343190/article/details/122821240