《程序员面试金典(第6版)》面试题 05.08. 绘制直线

题目描述

已知一个由像素点组成的单色屏幕,每行均有 w 个像素点,所有像素点初始为 0,左上角位置为 (0,0)。

现将每行的像素点按照「每 32 个像素点」为一组存放在一个 int 中,再依次存入长度为 length 的一维数组中。

我们将在屏幕上绘制一条从点 (x1,y) 到点 (x2,y) 的直线(即像素点修改为 1),请返回绘制过后的数组。

注意:

  • 用例保证屏幕宽度 w 可被 32 整除(即一个 int 不会分布在两行上)

示例1:

  • 输入:length = 1, w = 32, x1 = 30, x2 = 31, y = 0
    输出:[3]
    解释:在第 0 行的第 30 位到第 31 位画一条直线,屏幕二进制形式表示为 [00000000000000000000000000000011],因此返回 [3]

示例2:

  • 输入:length = 3, w = 96, x1 = 0, x2 = 95, y = 0
    输出:[-1, -1, -1]
    解释:由于二进制 11111111111111111111111111111111 的 int 类型代表 -1,因此返回 [-1,-1,-1]

提示:

  • 1 <= length <= 10^5
    1 <= w <= 3 * 10^5
    0 <= x1 <= x2 < w
    0 <= y <= 10

解题思路与代码

我是第一次做这种题型的,我看到后第一反应就是头皮发麻,这里面的变量有点小多,但再仔细看看好像自己有点思路。然后一系列硬着头皮去做,终于写出来了。下面我就慢慢介绍,我的思路是怎样的。还有就是,这道题需要用到位运算符与bitset数据结构的知识,不懂的同学可以去看看我写的这两篇文章:

  • C++标准库类型,bitset类型
  • 扫除盲点:教你理解位运算符

方法一:模拟法(使用数据结构bitset)

我自己写的这种方法有点笨,里面充满了各种条件判断。但胜在逻辑好懂,小白都能看懂。下面我来说一下我的解题思路。

首先由题意可知,每一行有 w 个字符,w 可以被32整除,也就是意味着 w 最小为32,不要被这个下面的提示吓到了。测试案例里没有w小于32的情况。

那么就可知一行会有 w / 32 个int ,这个就是我们等会要去遍历一行中int的个数。

那么由它给的length,我们就知道,这个像素矩阵一共有 length /(w /32) 行,这个就是我们要遍历的行的个数。

它们分别组成了两层的for循环,为的就是我们要找到对应行 y ,然后去将x1x2之间的像素点全部置为1

将到这里,就该讲讲如何去将x1 与 x2 之间的像素点置为1了。

我们要分情况去讨论,一共有这么几种情况:

  • 从未找到过x1,x2
  • 第一次找到x1,x2不出现在这个int中
  • 第一次找到x1,x2出现在这个int中
  • 已经找到过了x1,但是没找到过x2,x1,x2都不在这个int中
  • 第一次找到了x2
  • 已经找到过了x1,x2

我们要去处理这6种情况下,我们该如何往vector中去添加int。所以可想而知,我的代码有多复杂,其中要判断的边界条件有多少。废话就不多说了,我代码中的注释很详细的,剩下的就看代码吧。

代码如下:

class Solution {
public:
    vector<int> drawLine(int length, int w, int x1, int x2, int y) {
        int size = w / 32 ; //一行像素点中,有多少个int。
        vector<int> result ;
        int size_y = length / size; // 这个像素矩阵一共有多少行
        for(int i = 0; i < size_y; ++i){ //遍历每一行
            if(i == y){ //找到y所在的那一行
                bool flag1 = false; //设置标志位,如果在某一行找到过x1了,将x1置为true;
                bool flag2 = false;//设置标志位,如果在某一行找到过x2了,将x2置为true;
                for(int j = 0; j < size; ++j){ //遍历每一行中的int
                    bitset<32> num(0);
                    if(32*(j + 1) - x1 <= 0 && !flag1){ //在这个int中找不到x1,那肯定找不到x2,添加一个int,跳出本次循环
                        result.push_back(0);
                        continue;
                    }
                    if(32*(j + 1) - x1 > 0 && !flag1){ //第一次找到了x1,之后要去看看能不能找到x2
                        flag1 = true;//找到了x1,先将对应flag置为true
                        num.set(32*(j + 1) - x1 - 1); //先将x1这个点置为1。
                        if(32*(j+1) - x2 <= 0){ //找不到x2,从x1这个点往右侧,所有位置为1。添加int然后跳过本次循环。
                            for(int k = 32*(j+1) - x1 - 1; k >=0; --k){
                                num.set(k);
                            }
                            result.push_back((int)num.to_ulong());
                            continue;
                        }
                        if(32*(j+1) - x2 > 0){  //找到了x2,那就将x1到x2之间的点都置为1。添加int,然后跳过本次循环
                            flag2 = true;
                            for(int k = 32*(j+1) - x2 -1; k <= 32*(j + 1) - x1 - 1; ++k ){
                                num.set(k);
                            }
                            result.push_back((int)num.to_ulong());
                            continue;
                        } 
                    }
                    if(32*(j + 1) - x1 > 0 && flag1 && !flag2){//我已经找到过x1,但还没有找到x2
                        if(32*(j + 1) - x2 <= 0){ //说明x2不在这个int中
                            result.push_back(-1);
                            continue;
                        }
                    } 
                    if(32*(j + 1) - x2 > 0 && !flag2 && 32*(j + 1) - x2 < 32){ //我第一次找到了x2,我要把x2这个位向左所有位都置于1
                        num.set(32*(j + 1) - x2 - 1);
                        for(int k = 32*(j + 1) - x2 - 1 ; k < 32; ++k){
                            num.set(k);
                        }
                        result.push_back((int)num.to_ulong());
                        continue;
                    }
                    result.push_back(0);//我找到过x1,x2,但还要填0;
                    continue;
                }
            }
            if(i != y){
                for(int i = 0; i < size; ++i){
                    result.push_back(0);
                }
            }
        }
        return result;
    }
};

《程序员面试金典(第6版)》面试题 05.08. 绘制直线_第1张图片

复杂度分析

首先,我们注意到,在计算变量 size_y 时,使用了除法运算。因此,时间复杂度为 O ( 1 ) O(1) O(1)

然后,我们考虑主循环,即遍历每一行像素点的循环。在最坏情况下,我们需要遍历所有行,因此时间复杂度为 O ( l e n g t h ) O(length) O(length)。在每行像素点的循环中,我们需要遍历每个 32 位的整数,因此时间复杂度为 O ( w / 32 ) O(w/32) O(w/32)。在这个循环中,我们使用了 bitset,对于每个 32 位的整数,需要使用常数时间。因此,对于每个像素点,时间复杂度为 O ( 1 ) O(1) O(1)

综上所述,总时间复杂度为 O ( l e n g t h ⋅ w / 32 ) O(length \cdot w/32) O(lengthw/32)。空间复杂度方面,我们使用了一个大小为 w / 32 w/32 w/32 的 vector,因此空间复杂度为 O ( w / 32 ) O(w/32) O(w/32)

需要注意的是,这个分析是在最坏情况下的情况下,实际的时间复杂度可能会更低,取决于输入参数的具体值。

方法二:只处理y那一行的代码

我看了某大神的解法,就感觉到大神思考问题思考的太透彻了,我还有很多需要学习的地方,向大神学习。好了废话不多说,来讲讲大神的思路是什么样子的:

  • 首先,我重新再仔细读题后发现,真正需要处理像素点,它只在y那一行上。我们只需要针对y那一行去写我们的代码逻辑就好。
  • 我们先创建一个vector,将所有int存入其中,初始化为0,再用一个count去记录一行中有多少个int。
  • 之后我们用一个for循环,只去遍历y那一行中的像素点。并从x1为起始位置去遍历,直到x1<=x2 为止
  • 我们可以通过 res[y * count + i /32] 去找到x1所在的那个int,然后用去找到对应的二进制位 31 - i % 32 ,最后用位运算符将它置为1就好1 << 31 - i % 32
  • 这样循环变量,我们就成功的将y那一行的所有像素点都置为1啦,这道题对应的也就结束了
class Solution {
public:
    vector<int> drawLine(int length, int w, int x1, int x2, int y) {
        
        int count = w / 32; //一行中有多少int
        vector<int> res(length, 0);//先将所有的int都提前存进vector中,反正如果不是y那一行添加的int也都是0
        for(int i = x1; i <= x2 && i < length * 32; i++)//处理y那一行的每一个int
            //res[y * x + i /32] 这是为了找到x1和x2的那个int值在哪,之后方便用位运算符去修改其中的数据。
            // 1 << (31 - (i % 32)) 找到了对应的二进制位,将他改为1就好。
            res[y * conut + i / 32] |= (1 << 31 - i % 32);
        return res;
    }
};

《程序员面试金典(第6版)》面试题 05.08. 绘制直线_第2张图片

复杂度分析

使用了一个一维 vector 来存储所有的像素点,而不是使用一个二维数组或 bitset。这可以将空间复杂度降低为 O ( w / 32 ) O(w/32) O(w/32)

其次,只遍历了直线所在的那一行,而不是遍历所有像素点。这可以将时间复杂度降低为 O ( x ) O(x) O(x),其中 x x x 表示直线所在的一行中像素点的数量。

综上所述,代码的时间复杂度为 O ( x ) O(x) O(x),空间复杂度为 O ( w / 32 ) O(w/32) O(w/32)

总结

这道题是一道很考验细节处理的题。并且也考验了对题目要点总结的能力。做完了这道题我感觉到收益良多。

你可能感兴趣的:(#,算法题解析与个人做题技巧总结,面试,算法,c++)