数据结构和算法三十六

剑指 Offer 66. 构建乘积数组

题目:给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

示例:
      输入: [1,2,3,4,5]
      输出: [120,60,40,30,24]
提示:
      1、所有元素乘积之和不会溢出 32 位整数
      2、a.length <= 100000
解题思路:
本题的难点在于 不能使用除法 ,即需要 只用乘法 生成数组 B 。根据题目对 B[i] 的定义,可列表格,如下图所示。

根据表格的主对角线(全为 1 ),可将表格分为 上三角下三角 两部分。分别迭代计算下三角和上三角两部分的乘积,即可 不使用除法 就获得结果。
在这里插入图片描述
算法流程:
1、初始化:数组 B ,其中 B[0]=1 ;辅助变量 tmp=1 ;
2、计算 B[i] 的 下三角 各元素的乘积,直接乘入 B[i] ;
3、计算 B[i] 的 上三角 各元素的乘积,记为 tmp ,并乘入 B[i] ;
4、返回 B 。

复杂度分析:

  • 时间复杂度 O(N): 其中 N 为数组长度,两轮遍历数组 a ,使用 O(N) 时间。
  • 空间复杂度 O(1): 变量 tmp 使用常数大小额外空间(数组 b 作为返回值,不计入复杂度考虑)。
class Method{
    public int[] constructArr(int[] a) {
        if(a.length == 0) return new int[0];
        int[] b = new int[a.length];
        b[0] = 1;
        int tmp = 1;
        for(int i = 1; i < a.length; i++) {
            b[i] = b[i - 1] * a[i - 1];
        }
        for(int i = a.length - 2; i >= 0; i--) {
            tmp *= a[i + 1];
            b[i] *= tmp;
        }
        return b;
    }
}

剑指 Offer 29. 顺时针打印矩阵

题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。

示例 1:
      输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
      输出:[1,2,3,6,9,8,7,4,5]
示例 2:
      输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
      输出:[1,2,3,4,8,12,11,10,9,5,6,7]
限制:
      1、0 <= matrix.length <= 100
      2、0 <= matrix[i].length <= 100
方法一:模拟
可以模拟打印矩阵的路径。初始位置是矩阵的左上角,初始方向是向右,当路径超出界限或者进入之前访问过的位置时,顺时针旋转,进入下一个方向。

判断路径是否进入之前访问过的位置需要使用一个与输入矩阵大小相同的辅助矩阵 visited,其中的每个元素表示该位置是否被访问过。当一个元素被访问时,将 visited 中的对应位置的元素设为已访问。

如何判断路径是否结束?由于矩阵中的每个元素都被访问一次,因此路径的长度即为矩阵中的元素数量,当路径的长度达到矩阵中的元素数量时即为完整路径,将该路径返回。

class Method1{
    public int[] spiralOrder(int[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return new int[0];
        }
        int rows = matrix.length, columns = matrix[0].length;
        boolean[][] visited = new boolean[rows][columns];
        int total = rows * columns;
        int[] order = new int[total];
        int row = 0, column = 0;
        int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
        int directionIndex = 0;
        for (int i = 0; i < total; i++) {
            order[i] = matrix[row][column];
            visited[row][column] = true;
            int nextRow = row + directions[directionIndex][0], nextColumn = column + directions[directionIndex][1];
            if (nextRow < 0 || nextRow >= rows || nextColumn < 0 || nextColumn >= columns || visited[nextRow][nextColumn]) {
                directionIndex = (directionIndex + 1) % 4;
            }
            row += directions[directionIndex][0];
            column += directions[directionIndex][1];
        }
        return order;
    }
}

复杂度分析:

  • 时间复杂度:O(mn),其中 m 和 n 分别是输入矩阵的行数和列数。矩阵中的每个元素都要被访问一次。
  • 空间复杂度:O(mn)。需要创建一个大小为 m×n 的矩阵 visited 记录每个位置是否被访问过。

方法二:按层模拟
可以将矩阵看成若干层,首先打印最外层的元素,其次打印次外层的元素,直到打印最内层的元素。

定义矩阵的第 k 层是到最近边界距离为 k 的所有顶点。例如,下图矩阵最外层元素都是第 1 层,次外层元素都是第 2 层,剩下的元素都是第 3 层。

[[1, 1, 1, 1, 1, 1, 1],
 [1, 2, 2, 2, 2, 2, 1],
 [1, 2, 3, 3, 3, 2, 1],
 [1, 2, 2, 2, 2, 2, 1],
 [1, 1, 1, 1, 1, 1, 1]]

对于每层,从左上方开始以顺时针的顺序遍历所有元素。假设当前层的左上角位于 (top,left),右下角位于 (bottom,right),按照如下顺序遍历当前层的元素。
1、从左到右遍历上侧元素,依次为 (top,left) 到 (top,right)。
2、从上到下遍历右侧元素,依次为 (top+1,right) 到 (bottom,right)。
3、如果 left<right 且 top<bottom,则从右到左遍历下侧元素,依次为 (bottom,right−1) 到 (bottom,left+1),以及从下到上遍历左侧元素,依次为 (bottom,left) 到 (top+1,left)。

遍历完当前层的元素之后,将 left 和 top 分别增加 1,将 right 和 bottom 分别减少 1,进入下一层继续遍历,直到遍历完所有元素为止。
在这里插入图片描述

class Method2{
    public int[] spiralOrder(int[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return new int[0];
        }
        int rows = matrix.length, columns = matrix[0].length;
        int[] order = new int[rows * columns];
        int index = 0;
        int left = 0, right = columns - 1, top = 0, bottom = rows - 1;
        while (left <= right && top <= bottom) {
            for (int column = left; column <= right; column++) {
                order[index++] = matrix[top][column];
            }
            for (int row = top + 1; row <= bottom; row++) {
                order[index++] = matrix[row][right];
            }
            if (left < right && top < bottom) {
                for (int column = right - 1; column > left; column--) {
                    order[index++] = matrix[bottom][column];
                }
                for (int row = bottom; row > top; row--) {
                    order[index++] = matrix[row][left];
                }
            }
            left++;
            right--;
            top++;
            bottom--;
        }
        return order;
    }
}

复杂度分析

  • 时间复杂度:O(mn),其中 m 和 n 分别是输入矩阵的行数和列数。矩阵中的每个元素都要被访问一次。
  • 空间复杂度:O(1)。除了输出数组以外,空间复杂度是常数。

方法三:模拟、设定边界
解题思路:

根据题目示例 matrix = [[1,2,3],[4,5,6],[7,8,9]] 的对应输出 [1,2,3,6,9,8,7,4,5] 可以发现,顺时针打印矩阵的顺序是 “从左向右、从上向下、从右向左、从下向上” 循环。

  • 因此,考虑设定矩阵的“左、上、右、下”四个边界,模拟以上矩阵遍历顺序。
    在这里插入图片描述

算法流程:
1、空值处理: 当 matrix 为空时,直接返回空列表 [] 即可。
2、初始化: 矩阵 左、右、上、下 四个边界 l , r , t , b ,用于打印的结果列表 res 。
3、循环打印: “从左向右、从上向下、从右向左、从下向上” 四个方向循环,每个方向打印中做以下三件事 (各方向的具体信息见下表) ;
(1)根据边界打印,即将元素按顺序添加至列表 res 尾部;
(2)边界向内收缩 1 (代表已被打印);
(3)判断是否打印完毕(边界是否相遇),若打印完毕则跳出。
4、返回值: 返回 res 即可。

打印方向1. 根据边界打印2. 边界向内收缩3. 是否打印完毕
从左向右左边界l ,右边界 r上边界 t 加 1是否 t > b
从上向下上边界 t ,下边界b右边界 r 减 1是否 l > r
从右向左右边界 r ,左边界l下边界 b 减 1是否 t > b
从下向上下边界 b ,上边界t左边界 l 加 1是否 l > r

复杂度分析:

  • 时间复杂度 O(MN): M, N 分别为矩阵行数和列数。
  • 空间复杂度 O(1): 四个边界 l , r , t , b 使用常数大小的 额外 空间( res 为必须使用的空间)。

代码:

Java 代码利用了 ++ 操作的便利性,详情可见 ++i 和 i++ 的区别 ;

res[x++] 等价于先给 res[x] 赋值,再给 x 自增 1 ;
++t > b 等价于先给 t 自增 1 ,再判断 t > b 逻辑表达式。

class Method3{
    public int[] spiralOrder(int[][] matrix) {
        if(matrix.length == 0) return new int[0];
        int l = 0, r = matrix[0].length - 1, t = 0, b = matrix.length - 1, x = 0;
        int[] res = new int[(r + 1) * (b + 1)];
        while(true) {
            for(int i = l; i <= r; i++) res[x++] = matrix[t][i]; // left to right.
            if(++t > b) break;
            for(int i = t; i <= b; i++) res[x++] = matrix[i][r]; // top to bottom.
            if(l > --r) break;
            for(int i = r; i >= l; i--) res[x++] = matrix[b][i]; // right to left.
            if(t > --b) break;
            for(int i = b; i >= t; i--) res[x++] = matrix[i][l]; // bottom to top.
            if(++l > r) break;
        }
        return res;
    }
}

剑指 Offer 31. 栈的压入、弹出序列

题目:输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。

示例 1:
      输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
      输出:true
      解释:我们可以按以下顺序执行:
      push(1), push(2), push(3), push(4), pop() -> 4,
      push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
示例 2:
      输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
      输出:false
      解释:1 不能在 2 之前弹出。
提示:
      1、0 <= pushed.length == popped.length <= 1000
      2、0 <= pushed[i], popped[i] < 1000
      3、pushed 是 popped 的排列。
解题思路:
如下图所示,给定一个压入序列pushed和弹出序列popped,则压入/弹出操作的顺序(即排列)是 唯一确定 的。
在这里插入图片描述如下图所示,栈的数据操作具有 先入后出 的特性,因此某些弹出序列是无法实现的。
在这里插入图片描述考虑借用一个辅助栈 stack ,模拟 压入 / 弹出操作的排列。根据是否模拟成功,即可得到结果。

  • 入栈操作: 按照压栈序列的顺序执行。
  • 出栈操作: 每次入栈后,循环判断 “栈顶元素 = 弹出序列的当前元素” 是否成立,将符合弹出序列顺序的栈顶元素全部弹出。

由于题目规定 栈的所有数字均不相等 ,因此在循环入栈中,每个元素出栈的位置的可能性是唯一的(若有重复数字,则具有多个可出栈的位置)。因而,在遇到 “栈顶元素 = 弹出序列的当前元素” 就应立即执行出栈。

算法流程:
1、初始化: 辅助栈 stack ,弹出序列的索引 i ;
2、遍历压栈序列: 各元素记为 num ;
(1)元素 num 入栈;
(2)循环出栈:若 stack 的栈顶元素 = 弹出序列元素 popped[i] ,则执行出栈与 i++ ;
3、返回值: 若 stack 为空,则此弹出序列合法。
复杂度分析:

  • 时间复杂度 O(N): 其中 N 为列表 pushed 的长度;每个元素最多入栈与出栈一次,即最多共 2N 次出入栈操作。
  • 空间复杂度 O(N): 辅助栈 stack 最多同时存储 N 个元素。

代码:
题目指出 pushed 是 popped 的排列 。因此,无需考虑 pushed 和 popped 长度不同包含元素不同 的情况。

class Method{
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        Stack<Integer> stack = new Stack<>();
        int i = 0;
        for(int num : pushed) {
            stack.push(num); // num 入栈
            while(!stack.isEmpty() && stack.peek() == popped[i]) { // 循环判断与出栈
                stack.pop();
                i++;
            }
        }
        return stack.isEmpty();
    }
}

总结:

      今天晚上已经将剑指Offer的75道题已经更新完了!当然这并不是意味着数据结构和算法系列就已经结束了!数据结构和算法当然还有很多!我准备后续再更新数据结构和算法的其他模块!剑指Offer只是其中的一部分!还有各种的算法,如:ACM等等。明天开始准备为小伙伴们整理一波Java基础的学习路线,供初学的小伙伴们学习!对于在校想学习Java的小伙伴会有些帮助哦!小伙伴们的浏览、收藏、点赞、关注就是我前进道路上的最大动力!
      今天是最不幸的日子,早上刚朦胧的睁开眼,阳光普照一切似乎都很美好!可就在一瞬间的时间,网上处处报道袁隆平院士逝世的消息!就在10:30的时候散播谣言的平台公开道歉!这场因谣言产生的风波才算过去!因此,在任何事情没有确凿证据之前,先管好自己的嘴!不信谣、不传谣!
      就在下午14:00整,我手机上的一条信息震撼了全世界!官方报道袁隆平院士真正的离开了我们!一代伟人就这样永远的离开了我们!我的内心极其的沉痛!全国人民的内心也无比的沉重!如果没有袁隆平院士,不知道还有多少人可能到现在还填不饱肚子!正因为有了袁隆平院士,粮食产量日益增长,国家才真正解决了温饱问题!说起来天气也怪,下午的天由阴沉转变为雨天,在回到家之前,在路上又看到吴孟超院士也逝世了。在同一天的时间里共和国失去了两位得力的干将!他们也永远的离开了我们!
      两位院士为国家做出的贡献实在是太多太多了!一个让所有的人不再饿肚子的折磨;一个让所有的人不再受肝胆的病痛折磨。两位共和国院士的逝世对国家来说都是巨大的损失!或许这场雨是老天在为这两位院士的逝世而哭泣吧!在以后的日子里吾辈应当养成先辈良好的品德和思想!养成勤俭节约、淡泊名利的作风!争取做一个对国家对社会有贡献的人!逝去的先辈们,你们的大功大德我们后辈定将铭记在心,你们的丰功伟绩定将载入史册、流芳百世!
      臧克家说过:有的人活着,他已经死了!有的人死了,他还活着!虽然两位院士永久的离开了我们,但他们却活在了我们每个人的心中!吾辈应当自强、继续努力、奋发图强!争取早日成为国家的栋梁之材为国家和人民做一份真正的实事!
      向逝去的英雄致敬!愿逝去的英雄能够在天堂得以安息!
      最后,愿我们都能在各行各业中能够取得不同的成就,不负亲人、朋友、老师、长辈和国家的期望!能够用自身的所学知识为国家贡献出自己的一份力量!一起加油!
                                                                                                                       2021年5月22日夜

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值