算法总结-数组(剑指Offer)


这是一篇关于总结剑指Offer数组相关的算法题的文章。

剑指 Offer 13. 机器人的运动范围

原题链接

题目

地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

思路

通过题目和原题链接中的测试用例,我们不难发现这是一道典型的搜索题。对于搜索题,我们应该想到的是用DFS或者BFS进行解决。

题解

方法一:DFS

根据题目描述,我们可以模拟题目,我们假设一个5x5矩阵,阈值k=3,如果我们用DFS的话,就相当于“不撞南墙不回头”,我在下面画了一个图,
在这里插入图片描述
最开始,我们在(0,0)的位置,我们假设按照{右,下,左,上}的方向去试探。所以我们走的顺序应该是按照图中的下标走的。
当我们从1走到4的时候,发现不能往继续往右边走,并且4个方向都走不通了,那就回溯到3,发现可以走到5,接着就站在5的视角,发现可以走6,就一直按照这个想法。

本题的递归函数就是:首先站在(0,0)的视角,先往右试探,发现可以走,就以下一个为视角,继续做相同的事情。

代码:

class Solution {

public:
    using V = vector<int>;//定义一个模板
    using VV = vector<V>;    
    int dir[5] = {-1, 0, 1, 0, -1};//模拟四个方向

    int check(int n) {
        int sum = 0;

        while (n) {
            sum += (n % 10);
            n /= 10;
        }

        return sum;
    }

    void dfs(int x, int y, int sho, int r, int c, int &ret, VV &mark) {
        // 检查下标 和 是否访问
        if (x < 0 || x >= r || y < 0 || y >= c || mark[x][y] == 1) {
            return;
        }
        // 检查当前坐标是否满足条件
        if (check(x) + check(y) > sho) {
            return;
        }
        // 代码走到这里,说明当前坐标符合条件
        mark[x][y] = 1;
        ret += 1;

        for (int i = 0; i < 4; ++i) {
            dfs(x + dir[i], y + dir[i + 1], sho, r, c, ret, mark);
        }



    } 
    int movingCount(int sho, int rows, int cols)
    {
        if (sho <= 0) {
            return 0;
        }

        VV mark(rows, V(cols, -1));//定义一个二维数组作标记,默认为0。
        int ret = 0;
        dfs(0, 0, sho, rows, cols, ret, mark);
        return ret;
    }
};

方法二:BFS
当前图的遍历算法还有BFS,所以也可以用BFS做。方法一实例的图,用BFS就是如下这样:
在这里插入图片描述
广度优先要借助队列。
代码:

class Solution {
public:
    int dir[5] = {-1, 0, 1, 0, -1};
    int check(int n) {
        int sum = 0;

        while (n) {
            sum += (n % 10);
            n /= 10;
        }

        return sum;
    }
    int movingCount(int sho, int rows, int cols)
    {
        if (sho <= 0) {
            return 0;
        }

        int ret = 0;
        int mark[rows][cols];
        memset(mark, -1, sizeof(mark));
        queue<pair<int,int>> q;
        q.push({0, 0});
        mark[0][0] = 1;


        while (!q.empty()) {
            auto node = q.front();//  auto [x, y] = Q.front();
            q.pop();
            // 每次保证进队列的都是满足条件的坐标
            ++ret;

            for (int i = 0; i < 4; ++i) {
                int x = node.first + dir[i];
                int y = node.second + dir[i + 1];

                if (x >= 0 && x < rows && y >= 0 && y < cols && mark[x][y] == -1) {
                    if (check(x) + check(y) <= sho) {
                        q.push({x, y});
                        mark[x][y] = 1;
                    }
                }
            }
        }

        return ret;
    }
};

方法三:递推
考虑到方法一提到搜索的方向只需要朝下或朝右,我们可以得出一种递推的求解方法。
定义 vis[i][j] 为 (i, j) 坐标是否可达,如果可达返回 1,否则返回 0。

首先 (i, j) 本身需要可以进入,因此需要先判断 i 和 j 的数位之和是否大于 k ,如果大于的话直接设置 vis[i][j] 为不可达即可。

否则,前面提到搜索方向只需朝下或朝右,因此 (i, j) 的格子只会从 (i - 1, j) 或者 (i, j - 1) 两个格子走过来(不考虑边界条件),那么 vis[i][j] 是否可达的状态则可由如下公式计算得到:

vis[i][j]=vis[i−1][j] or vis[i][j−1]

即只要有一个格子可达,那么 (i, j) 这个格子就是可达的,因此我们只要遍历所有格子,递推计算出它们是否可达然后用变量 ans 记录可达的格子数量即可。

初始条件 vis[i][j] = 1 ,递推计算的过程中注意边界的处理。

class Solution {
    int get(int x) {
        int res=0;
        for (; x; x /= 10){
            res += x % 10;
        }
        return res;
    }
public:
    int movingCount(int m, int n, int k) {
        if (!k) return 1;
        vector<vector<int> > vis(m, vector<int>(n, 0));
        int ans = 1;
        vis[0][0] = 1;
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if ((i == 0 && j == 0) || get(i) + get(j) > k) continue;
                // 边界判断
                if (i - 1 >= 0) vis[i][j] |= vis[i - 1][j];
                if (j - 1 >= 0) vis[i][j] |= vis[i][j - 1];
                ans += vis[i][j];
            }
        }
        return ans;
    }
};

练习1 练习2

剑指 Offer 59 - I. 滑动窗口的最大值

原题链接

题目

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

思路

根据题目描述,我们很容易想到暴力方法。并且也很轻松的就可以写出来。如果数组的大小是n,窗口的大小是size,那么窗口的数量就是 n - size + 1。

当然还有其他的做法,我们的优化应该放在窗口内最大值的比较上去。

题解

方法一:暴力方法
根据题目描述,我们很容易想到暴力方法。并且也很轻松的就可以写出来。如果数组的大小是n,窗口的大小是size,那么窗口的数量就是 n - size + 1。

代码:


class Solution {
public:
    vector<int> maxInWindows(const vector<int>& num, unsigned int size)
    {
        vector<int> ret;
        if (num.size() == 0 || size < 1 || num.size() < size) return ret;
        int n = num.size();

        for (int i = 0; i + size - 1 < n; ++i) {
            int j = i + size - 1;
            int max_val = num[j];
            for (int k = i; k < j; ++k) {
                max_val = max(max_val, num[k]);
            }
            ret.push_back(max_val);
        }
        return ret;
    }
};

方法二:单调队列
方法一种存在很多大量重复计算,比如说,对于数组,假设我们当前遍历到下标i,对于下标i+1的元素(假设i和i+1都在同一个窗口),如果比arr[i]大,说明了什么?
如果arr[i+1] 已经大于了 arr[i], 那么还要arr[i]有什么用.就有点“既生瑜何生亮”的感觉。
如果arr[i+1] < arr[i]呢?显然arr[i]还是需要保留的。为什么呢?
因为又可以arr[i] 对于下一个arr[i+1]所在的窗口来说,arr[i]已经失效了。

假设这里有那么一个容器可以保留上述操作。

  1. 遍历数组的每一个元素,
  2. 如果容器为空,则直接将当前元素加入到容器中。
  3. 如果容器不为空,则让当前元素和容器的最后一个元素比较,如果大于,则将容器的最后一个元素删除,然后继续讲当前元素和容器的最后一个元素比较
  4. 如果当前元素小于容器的最后一个元素,则直接将当前元素加入到容器的末尾
  5. 如果容器头部的元素已经不属于当前窗口的边界,则应该将头部元素删除
    总结一下,首先容器中放的元素应该是单调递减的。然后还有删除容器头部元素和最后一个元素的操作。因此,这样的数据结构就是双端队列。
    在这里插入图片描述
    代码:
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> ans;
        deque<int> q;
        int size = nums.size();
        if(size == 0) return ans;
        for(int i = 0;i<size;i++)
        {
            while(!q.empty()&&nums[q.back()]<nums[i]){
                q.pop_back();
            }
            q.push_back(i);
            if(q.front()+k<=i){
                q.pop_front();
            }
            if(i+1>=k){
                ans.push_back(nums[q.front()]);
            }
        }
        return ans;
    }
};

练习1

剑指 Offer 66. 构建乘积数组

原题链接

题目

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B 中的元素 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

思路

根据题目描述,如果可以使用除法,就很简单。但是要求不能使用。

假设:

left[i] = A[0]*...*A[i-1]
right[i] = A[i+1]*...*A[n-1]

所以:

B[i] = left[i] * right[i]

这样就避免使用了除法。但是如果对每个B[i], 0<=i<n,都这么求,显然时间复杂度太高。
我们把整个结果画到下面图:
在这里插入图片描述
可知:

left[i+1] = A[0]*...A[i-1]*A[i]
right[i+1] = A{i+2]*...*A[n-1]

于是,

left[i+1] = left[i] * A[i]
right[i] = right[i+1] * A[i+1]

所以,我们可以先把所有的left[i]求出,right[i]求出。

题解

代码:


class Solution {
public:
    vector<int> multiply(const vector<int>& A) {
        vector<int> B(A.size(), 1);
        for (int i=1; i<A.size(); ++i) {
            B[i] = B[i-1] * A[i-1]; // left[i]用B[i]代替
        }
        int tmp = 1;
        for (int j=A.size()-2; j>=0; --j) {
            tmp *= A[j+1]; // right[i]用tmp代替
            B[j] *= tmp;
        }
        return B;
    }
};
剑指 Offer 03. 数组中重复的数字

原题链接

题目

找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

思路

题目中含有重复的字眼,第一反应应该想到哈希,set。这里我们用哈希来解。

题解

方法一:哈希+遍历

算法步骤:

开辟一个长度为n的vector, 初始化为false
遍历数组,第一次遇到的数据,对应置为true
如果再一次遇到已经置为true的数据,说明是重复的。返回即可。

代码:

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        int length = nums.size();
        vector<bool> flags(length,false);
        int ans;
        for(int i = 0;i<length;i++)
        {
            if(!flags[nums[i]]){
                flags[nums[i]] = true;
            }else{
                ans =  nums[i];
                break;
            }
        }
        return ans;
    }
};

方法二:in-place算法

方法一中的一个条件我们没有用到。也就是数据的范围是0-n-1。所以我们可以这么做:

  1. 设置一个指针i指向开头0,
  2. 对于arr[i]进行判断,如果arr[i] == i, 说明下标为i的数据正确的放在了该位置上,让i++
  3. 如果arr[i] != i, 说明没有正确放在位置上,那么我们就把arr[i]放在正确的位置上,也就是交换 arr[i]
    和arr[arr[i]]。交换之后,如果arr[i] != i, 继续交换。
  4. 如果交换的过程中,arr[i] == arr[arr[i]],说明遇到了重复值,返回即可。

如下图:

在这里插入图片描述
代码:

class Solution {
public:
 // Parameters:
 //        numbers:     an array of integers
 //        length:      the length of array numbers
 //        duplication: (Output) the duplicated number in the array number
 // Return value:       true if the input is valid, and there are some duplications in the array number
 //                     otherwise false
 bool duplicate(int numbers[], int length, int* duplication) {
     for (int i=0; i<length; ++i) {
         // 不相等就一直交换
         while (i != numbers[i]) {
             if (numbers[i] != numbers[numbers[i]]) {
                 swap(numbers[i], numbers[numbers[i]]);
             }
             else {
                 *duplication = numbers[i];
                 return true;
             }
         }

     }
     return false;
 }
};
剑指 Offer 57. 和为s的两个数字

原题链接

题目

输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

思路

因为数组是有序的,所以可以用双指针。

题解

具体步骤如下:

  1. 初始化:指针i指向数组首, 指针j指向数组尾部
  2. 如果arr[i] + arr[j] == sum , 说明是可能解
  3. 否则如果arr[i] + arr[j] > sum, 说明和太大,所以–j
  4. 否则如果arr[i] + arr[j] < sum, 说明和太小,所以++i

代码:

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        int length = nums.size();
        int left = 0;
        int right = length -1;
        vector<int> ans;
        while(left<right)
        {
            if(target>nums[left]+nums[right])
            {
                ++left;
            }else if(target<nums[left]+nums[right]){
                --right;
            }
            else{
                ans.push_back(nums[left]);
                ans.push_back(nums[right]);
                break;
            }
        }
        return ans;
    }
};

练习一

更新中

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值