美文网首页Fuck iOS EveryDay
算法题心得 - 数组 哈希表 字符串

算法题心得 - 数组 哈希表 字符串

作者: bigonelby | 来源:发表于2018-06-15 18:06 被阅读39次

最近在准备面试的时候,特意的留意了一下算法,现在越来越多的公司在面试的时候,很看重算法能力。这篇文章,算是自己在阅读《剑指offer》后的一点点心得吧。

数组 哈希表

数组的哈希表算是最简单的两种数据结构了。这两种数据结构有一个非常重要的特点,就是可以通过O(1)的时间复杂度,找到某个值。

如何理解这个O(1)呢?就是如果我希望找到长度为n的数组array的下标为k的值,那我直接引用array[k]就可以找到它,这个查找的速度和array的大小n没有关系。这就是O(1)的时间复杂度。

如果需要遍历一遍数组才能找到想要的值,则时间复杂度就是O(n)了,如果需要遍历两遍,则时间复杂度是O(n^2)。提到了时间复杂度,就不得不提提空间复杂度。时间和空间总是此消彼长的,我们做优化的时候,经常根据实际的项目要求,用时间换空间,或用空间换时间。如果真的能同时降低时间复杂度和空间复杂度,那真的是非常好的算法了。

对于数组和哈希表具有O(1)时间复杂度的查找特性,我们经常可以通过辅助数组或哈希表的方法,达到空间复杂度换时间复杂度的目的。比如如果一个算法,需要O(n2)的时间复杂度,和O(1)的空间复杂度,那么我们在优化的时候,一个基本的思路,就是能否通过一个辅助的数组或哈希表,即O(n)的空间复杂度,将时间复杂度从O(n2)降为O(n)。

数组中,最常见的算法题,就是各种排序了,我觉得我们至少要清楚快速排序法,以及冒泡排序法能最基本的排序算法。很多数组类型的问题,都可以通过这种问题衍生。

字符串

和数组最接近的数据结构,也是和实际项目息息相关的,大概就是字符串了。这类问题通俗易懂,又可以考察数组和指针的基本用法,很多面试题的解答思路也非常巧妙,因此这部分内容是不容忽视的。

比如《剑指offer》中的面试题5

题目:请实现一个函数,把字符串中的每个空格替换成"%20"。例如,输入"We are happy.",则输出"We%20are%20happy."

对于任何题目,我们首先要考虑异常情况,一个优秀的解答,一定要具备正确性,以及鲁棒性。正确性是最基本的要求,也就是说输入和输出在正常的情况下是满足条件的,而鲁棒性就要求对异常的考虑了。实际上在实际工作中,对异常情况的考虑,也是非常重要的。

对于这道问题,首先需要考虑的就是,如果输入是null,应该如何处理?是否可以改变原有字符串,还是输出新的字符串?按照题目要求,字符串的长度将变大,是否输出的空间足够容纳这些字符串?

假如我们考虑可以破坏原有的数据,那么可以设计如下的函数

int replaceSpace(char* src, int length);

这里,src为源字符串,以及最后输出的字符串,length是src的大小,返回值设置为int类型,可以返回输出的字符串的大小,特别的,如果是-1可以认为是转换失败。

转换的思路,有两种,一种是从头到尾的遍历,将字符串替换为%20,这种方法,每次遇到空格的时候,都要插入新的字符,并且将后面的所有字符都往后依次挪位,这样的做法,没有用额外的存储空间,因此空间复杂度为O(n),而由于需要遍历一遍字符串,并且在每次遍历的时候,都有可能向后移动所有的字符,因此是一个双重循环,因此时间复杂度为O(n2)。

当然我们会考虑优化的方案,是否可以将时间复杂度降为O(n)呢,我们想到可以从尾向前遍历数组,我们首先可以遍历一遍字符串,找到空格的个数,从而计算出最终的字符串的长度,在这里,由于遍历了一遍字符串,因此时间复杂度为O(n),确认了最终的字符串长度后,我们可以定义两个指针,一个指向源字符串的尾部,另一个指向最终字符串的尾部,将源尾部的字符不断的拷贝到最终尾部,如果遇到空格,则替换为%20,这样,当两个尾指针相等时,循环结束。这样的拷贝,也用了O(n)的时间复杂度。因此总共用了O(n) + O(n)的时间复杂度,实际上还是O(n)。因为O(n) + O(n)和O(n)是同阶的。

这样,我们通过O(n)的时间复杂度,以及O(1)的空间复杂度,完成了这个空格的转换。在这道题中,我们运用了两个指针的技巧,这个技巧非常常用,我们在后面的算法中还会遇到类似的解决思路。

另一道我觉得非常好的题目,是面试题17

题目:输入数字n,按顺序打印出从1到最大的n位十进制数。比如输入3,则打印出1、2、3一直到最大的3位数999.

这个题看似很简单,实际上确是暗藏陷阱。这种看似简单的题目,就是很容易理解题目的含义,我们更要特别留意,找到题目中可能存在的陷阱。对于这种类型的题目,如果不仔细思考,找到关键,就很容易上当。这类问题是典型的数字和字符串相结合的问题,一个直观的方案,就是找到n位最大树,即10n - 1,然后从1开始打印,一直到这个最大的数。看起来道理完全讲的通,似乎很完美的解决思路,却已经在不知不觉中掉入了出题人的陷阱。

你找到的那个数10n - 1,真的可以用计算机的数字来描述吗?简单的用int可以吗?显然不可以,因为n可以很大,则10n - 1就是一个更大的数,显然完全可以超出int的范围。当没有一个合适的整型结构可以容纳任意大的数字的时候,我们就要考虑万能的字符串了。不管多大的数,我们当然都可以通过字符串来承载,这里就涉及到一个问题,就是如何完成一个字符串表示的数字的累加?实际上这也不难,我们只要模拟数字的每一位相加,并且进位就可以了。

这种字符串和整数相结合的题目,可以很好的考察对字符串以及数字的理解,类似的题目,比如输入一个整数,转换成字符串,这些看似简单的题目,实际上都暗藏杀机,需要仔细的考虑清楚,不要掉入陷阱。

再和大家分享一道题,是面试题19

题目:请实现一个函数用来匹配包含'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和”ab*ac*a”匹配,但与"aa.a"和"ab*a"均不匹配。

正则匹配是一个非常重要的领域,在实际应用中也非常复杂。这个题目挑选了正则匹配中的两个典型符号'*'以及'.',实际上已经大大化简了正则匹配的难度。虽然已经化简了难度,但对于没有接触过正则表达式的同学,还是比较复杂,尤其是'*’,由于'*'是和它上一个字符强关联的,可以是零个或多个,所以组合起来情况比较复杂。

这个题目的另一个特点是,题目本身构成了子问题。比如,判断aaa和a.a是否匹配,则可以将问题转换为,首字母a和a匹配,然后判断剩余的字符串是否匹配,也就是aa和.a是否匹配,这样,问题转换成了子问题。满足这种条件的问题,我们往往可以采用递归的思想对题目进行解答。

确定了通过递归方法的解题思路,我们再继续分析具体的思路,也就是如何将问题转换为子问题。我觉得递归问题有两个要点,一个就是终止判断,另一个就是将问题分解为子问题。

我们可以做如下的设计

bool isMatch(char* src, char* pattern);

其中src为输入的字符串,而pattern为输入的正则表达式,返回值是bool类型,表明是否匹配。解任何问题,首先就是要做异常处理,比如当src或者pattern为null的时候,我们应该判断匹配还是不匹配?这点我们可以和面试官交流。

异常情况处理完毕了,我们就要考虑递归问题的终止条件了,实际上终止条件有两种,其一是*src == '\0' && *pattern == '\0',这种情况下,表明src和pattern都成功的匹配到了最后,因此是match的,返回true。另一种情况是*src != '\0' && *pattern == '\0',也就是说src还没有走到最后,但是pattern已经走到了最后,这种情况下,显然两者是不匹配的,直接返回false。

说到这里,可能有的同学会想,如果*src == '\0' && *pattern != '\0'的情况是否也是递归的终止条件呢?这种情况下,src已经走到最后,但是pattern还没有走完,那么在这种情况下,我们可以判断两者是否匹配吗?答案是不一定。比如""和"a*",字符串已经走到最后,但是pattern还有后面的"a*",在这种情况下,两者也是匹配的,因为'*'可以表示零个前面的字符,也就是说"a*"也可以理解为""。因此在这种情况下,我们是不能轻易下结论的。因此,我们在分析问题的时候,一定要找到递归终止的真正条件,对于一些伪终止条件,我们要有自己的判断。

解决完异常处理,以及递归的终止判断,最后要做的就是找到转换为子问题的方法。前面已经分析过,如果有'*'的时候,情况就比较复杂,因此我们可以分类讨论。首先,就是如果src[1] != '*'的情况,在这种情况下,比较容易处理,就是要判断(*src == *pattern) || (*pattern == '.' && *src != '\0'),这里的判断就是两个字符是否相等,或者pattern为'.'的情况,因为'.'在正则表达式中可以代表所有的字符。如果两者相等,我们就将问题转换为了子问题,即isMatch(++src, ++pattern),我们成功的将src和pattern各往后推进了一位,问题转换为了子问题。如果上面的判断不相等,说明不匹配,直接返回false。

最后,处理最复杂的情况,也就是src[1] == '*'的情况,实际上这种情况下,也可以转换为子问题,考虑到'*'可代表0个前面的字符,问题可以转换为isMatch(src, pattern + 2)。另外,考虑到'*'还可以代表多个前面的字符,问题还可以转换为isMatch(++src, pattern),这样,这两种情况完全覆盖了'*'的含义,因此问题也成功的转换为了子问题。

于是我们做了异常处理,做了递归的终止处理,并且可以将问题转换为子问题。有了这三部曲,这个问题就可以圆满的解决了。

相关文章

  • 算法题心得 - 数组 哈希表 字符串

    最近在准备面试的时候,特意的留意了一下算法,现在越来越多的公司在面试的时候,很看重算法能力。这篇文章,算是自己在阅...

  • 高频题

    字符串 数学 数组 二叉树 二分查找 链表 动态规划 贪心算法 堆 广度优先搜索 深度优先 哈希表 回溯算法 设计

  • 算法题心得 - 链表

    上篇文章介绍了数组,哈希表,字符串相关的算法,这篇文章介绍另一个重要的数据结构,链表 链表特点 链表,和数组相比较...

  • 算法-哈希表算法总结

    1 哈希表模拟 思路:通过设计哈希表,模拟O(1)时间复杂度的哈希表。 2 数组作为哈希表 思路:数组就是简单的哈...

  • 1. 数据类型

    修饰符 数据类型 字符串(String) 哈希表(Object) 数组(Array) 数字(Number) 布尔(...

  • LeetCode: 36 有效的数独

    【记录性文章-数组】 代码思路:本质上还是判断数组内重复的题,所以还是使用一边判断是否在哈希表中,一边遍历向哈希表...

  • LeetCode ---- 热门标签分类

    标签分类(涉及>=50题) 1.数组。2.动态规划。3.数学。4.字符串。5.树。6.哈希表。7.深度优先搜索。8...

  • 算法 1.8 无重复字符的最长子串【leetcode 3】

    题目描述 给定一个字符串,请你找出其中不含有重复字符的最长子串的长度 数据结构 数组、指针、哈希表 算法思维 双指...

  • 数组,哈希表,字符串

    1,数组 1.1,实现一个动态扩容的数组 1.2,实现一个大小固定的有序数组,支持动态增删改操作 1.3,实现两个...

  • 哈希表

    今天做华硕的笔试题时,有道题是介绍哈希表和实现它的几个算法,虽然以前查过相关知识,但只是大概理解。 哈希表的概念 ...

网友评论

    本文标题:算法题心得 - 数组 哈希表 字符串

    本文链接:https://www.haomeiwen.com/subject/frppeftx.html