图解一道今日头条算法面试题


前几天听同事分享了一道今日头条的算法面试题,感觉非常有趣,今天也分享给大家,题目是这样的:

给定一个有序数组,数组中有正数、负数或者0,对数组中所有的数求平方后问有多少个不同的值。

比如对于数组[-1,0,1,1,1,1],对数组求平方后为[1,0,1,1,1,1],那么最终的结果是2,因为最后只有0和1两个不同的数;

对于数组[-1,0,1,2,3],对数组求平方后为[1,0,1,4,9],那么最终的结果是4,因为最后数组中为0,1,4,9这四个不同的数;

同事提到只有时间复杂度为O(n),空间复杂度为O(1)的算法才能通过面试,在听到这个题目后首先想的就是如果在面试中自己遇到这个题目该如何解决?

我是如何思考的

如果在面试中遇到这个题目我会先给出一个最简单的解法,也就是直接对数组中的每一个数字求平方,然后放到map中,最后求一下map的大小就可以了,整个过程如图所示:

在这里插入图片描述

实际上这种解法有个问题,那就是对数字求平方和容易导致溢出,而实际上我们根本就无需求平方,实际上只需要求出绝对值就可以了,只要两个数字的绝对值相等,那么这两个数字的平方也一定相等。因此我们只需要将上图中的第一步由求平方改为求绝对值就可以了。

这个算法相对简单,也很容易想到,实际复杂度为O(n)满足要求,但是由于我们使用了map,因此空间复杂度为O(n),不能满足面试要求的O(1),那么该如何改进呢?

在往下看之前你要首先自己想一想该如何改进。

优化算法:利用数组有序

不要忘了题目给定的是一个有序数组,上图中的算法没有利用到这一性质。

我的想法是这样的,给定一个有序数组,如果这个数组中没有负数,那么这个题目是极其简单的,因为在一个有序数组中相同的数组一定是相邻的

比如对于数组[0,1,1,1,1],其中四个1是相邻的,我们只需要在遍历数组的时候不计入重复的数字就可以了。

但是现在问题的关键在于数组中还有负数,对于负数该如何处理呢?很简单,我们只需要将负数部分转为正数就可以了,这样我们就得到了两个子数组,这两个子数组都是有序的,然后将这两个有序子数组原地合并,这样就得到了一个不包含负数的有序数组,问题圆满解决,如图所示:

在这里插入图片描述
 

问题

看上去看正确有没有,没有额外使用空间,而且时间复杂度是O(n),但是,这里有一个问题,这个问题出在了上图中的第4步,也就是原地合并数组中的两个部分,当时思考的时候没有意识到,那就是没有办法在不借助额外内存空间的情况下合并两个有序子数组,至少我没有想到这样的方法,如果你想到了请务必在公众号后台留言指正。

再次思考

发现这个问题后上面的解法从第4步之后都是不正确的了,但是我直觉这个解法距离正确答案应该不远了,既然我们没有办法在不借助外部空间的情况下合并两个有序子数组,那么真的有必要去合并两个子数组吗,意识到这一点后,惊喜的发现在不进行合并的情况下问题能够解决,只需要设置两个指针i和j,分别指向两段数组的起始位置,设置num用来记录最后结果,在上图第3步逆转顺序后如图所示:

在这里插入图片描述

聪明的你看到上图就能明白这个算法了。

顺便说一句,双指针是数组类算法题中非常常见的解题思路。

进一步优化

在想到双指针可以解决这个问题后我进一步意识到其实根本没有必要找到是负数的那一部分子数组然后取反再逆转,实际上这些都没有必要,我们只需要比较数组中两个数字的绝对值就可以了,有的同学可能会说那逆转数组也没有必要吗,是的,没有必要,上图中两个指针是相同方向前进,在不逆转负数那一部分数组的情况下,只需要两个指针逆向前进就可以了,如图所示:

在这里插入图片描述

这样我们最终得到了一个很简单很容易编写代码的解决方法。

有了以上分析,代码就非常简单了。

代码实现

int Solution(vector<int>& arr) {
    if (arr.size() == 0)
        return 0;
    int i = 0;
    int j = arr.size() - 1;
    int num = 0;
    while(i <= j) {
        ++num;
        if (abs(arr[i]) > abs(arr[j])) {
            ++i;
            while(i <= j && arr[i-1] == arr[i])
                ++i;
        } else if (abs(arr[i]) < abs(arr[j])) {
            --j;
            while(i<= j && arr[j]==arr[j+1]){
                --j;
            }
        } else {
            ++i;--j;
           while(i <= j && arr[i-1] == arr[i])
                ++i;
           while(i<= j && arr[j]==arr[j+1])
                --j;
        }
    }
    return num;
}

代码非常简单,唯一需要注意的地方在于下标的移动。

总结

在这里详细的讲述了我对这道题目的思考过程,从这里也可以看到,除了提前见过这个题目,否则有时我们可能没有办法一下就想到最优解,要想找到最优解你需要不断的尝试验证,这个过程是非常有必要的,有很多同学可能学过很多关于数据结构和算法的资料但是始终不得要领,很关键的一点就是思考过程是作者的而不是你的,没有什么东西是轻而易举就能学会的,除非其真的很简单,但是,数据结构与算法并不属于这一类。

更多计算机内功文章,欢迎关注微信公共账号:码农的荒岛求生

在这里插入图片描述
彻底理解操作系统系列文章
1,什么程序?
2,进程?程序?傻傻分不清
3,程序员应如何理解内存:上篇
4,程序员应如何理解内存:下篇
 
 

计算机内功决定程序员职业生涯高度

发布了38 篇原创文章 · 获赞 30 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/github_37382319/article/details/103199146