数据结构与算法
程序 = 算法 + 数据结构,这是程序的经典解释。所谓数据结构是以某种形式将数据组织在一起的集合,它不仅存储数据,还支持访问和处理数据的操作;所谓算法,是为求解一个问题需要遵循的、被清楚指定的简单指令的集合。
一、知识结构及面试题目分析
在专栏开始之前,笔者也曾与课程顾问进行了深度讨论,要不要保留这一章。倒不是说算法与数据结构不重要,主要是基于以下两方面的考虑:
一是契合性。数据结构与算法对于计算机人才来说很重要,不过它内容丰富,只写一节或者几节其实是完全讲不透的,甚至于只是讲某个算法也讲不透,所以放在专栏中是否合适;
二是必要性。其实并非所有公司的面试都会考算法,对算法有要求的面试通常有两种:一是应届毕业生校招,因为很多应届同学缺乏项目经验,而且不同专业不同人所擅长的语言也不一样,而校招一般会有笔试 / 机试,这两类面试类型通常不会太考语言层面的特性(除非应届生简历上注明了熟悉某种语言),而算法则可以很好地屏蔽语言细节,而且对应届生的逻辑思维、潜力、学习能力等都有很好的区分度;二是某些对算法情有独钟的互联网公司(比如说字节跳动),实际上更多公司,比如说传统 it 公司、国企银行、一般的互联网公司等,在面试中对纯算法其实考察得不多。从个人角度来看,纯考算法的开发岗面试其实并无必要。
但是最终决定仍然保留这个章节,因为对于 Java 工程师来说,算法是很重要的基本功,虽然实际工程中很少直接照搬纯算法面试题的实现,但是对于设计系统来说,算法题中的思想还是有一定借鉴意义的。此外,从之前的留言来看,订阅本专栏的还有不少在校学生,在校招中还是经常考察算法的。
常见的算法题包括数组操作、排序、动态规划、树、栈、链表、堆、字符串、回溯算法、递归、深度优先搜索等等。正是由于算法题的多样性和复杂性,本章节和其他章节的安排也有一些差异,花了较多的时间对算法考察的面试场景进行了说明,并对算法考察的面试场景进行了说明。在第二部分典型面试例题及思路分析则选择笔者在面试中遇到的题目。
二、典型面试例题及思路分析
问题 1:给定一个链表,判断链表中是否有环?
使用快慢指针。
private boolean isLoopInListByslowAndFastPointer(Node head) {
if (head == null) { // head是指定链表的头指针
return false;
}
Node slow = head; //慢指针
Node fast = head.next; // 快指针
while (fast != null && fast.next != null) {
if (slow.equals(fast)) {
return true;
}
slow = slow.next; //慢指针每次走一步
fast = fast.next.next; // 快指针每次走两步
}
return false;
}
点评:
这道题是一道 easy(简单) 级别的题目,除了上述的快慢指针方案外,还可以使用哈希表方案:遍历所有结点。并在哈希表中存储每个结点的引用 / 内存地址。如果当前结点为空结点 null(即已检测到链表尾部的下一个结点),那么我们已经遍历完整个链表,说明该链表不是环形链表;如果当前结点的引用已经存在于哈希表中,该链表为环形链表;
这道题还有一个常见的衍生题目,即返回链表开始入环的第一个节点。
问题 2:给你一个包含 n 个整数的升序数组,判断数据中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请找出所有满足条件且不重复的三元组。
List<List<Integer>> findThreeNumForZero(int[] nums){
int len = nums.length;
List<List<Integer>> result = new ArrayList<>();
if (len < 3) {
return result;
}
for (int i = 0; i < len - 2; i++) {
if(nums[i] > 0) { //由于是升序数组,所以当第一个加数大于0时,和必然大于0,结束循环
break;
}
if (i > 0 && nums[i] == nums[i - 1]) { // 相邻两个元素值一样,三元组重复,跳过
continue;
}
int start = i + 1;
int end = len - 1;
while (start < end) {// 对于每个元素
int sum = nums[i] + nums[end] + nums[start];
if (sum > 0) { // 三个数求和大于0,说明和较大,下一次尝试需要取较小的数
end--;
} else if (sum < 0) { // 三个数求和小于0,说明和较小,下一次尝试需要取较大的数
start++;
} else {
List<Integer> zeroList = Arrays.asList(nums[i], nums[start], nums[end]);
result.add(zeroList);
start++;
end--;
// 跳过重复值。
while (start < end && nums[start] == nums[start - 1]) {
start++;
}
while (start < end && nums[end] == nums[end + 1]) {
end--;
}
}
}
}
return result;
}
点评:
由于是升序数组(如果是无序数组需要先排序),所以在锚定某个元素(nums [i])后分别从数组两端取两个元素并计算两数之和,大于 0 则 end–,否则 start++,遍历所有和为 0 元素组合。
那么如何解决结果中有重复值的情况呢?重复的情况必然元素相等,对于锚定元素直接判断和前一个元素是否相等,对于遍历的 start 和 end 元素只需要在在找到和为 0 的三元组的情况下再去判断相邻元素是否相等,如果相等则说明会重复,则跳过该元素。
数组求和问题也是很有代表性的算法题,相似的算法题还有两数求和为 0、四数求和为 0、三数求和为 K,三数之和最接近目标值等。
三、总结
“无它,唯手熟尔”,这可以视为算法面试题的方法论,特别是对于算法基础没那么好的候选者来说,一定要多加练练,leetcode 或者 力扣中文版都有很好的题库,也都有很多前人总结的经验,是算法学习与练习很好的资料。
当然练习也有练习的方法, 现在力扣有 1000 + 的例题,而且不少题目属于 hard(困难) 级别,全部练习肯定是练不过来,而且效率不高,对于面试来说可以考虑如下优化解法:
(1)如果你基础差从未练习过题目,建议先选择 easy(简单) 题按照顺序去做,在练习的过程中强化自己的基础知识;
(2)如果有了一定的基础,根据标签(链表、哈希表、堆、栈、树等)按需所取,选取一些简单中等的题目强化相关数据结构知识;
(3)如果能力较强,可以选择 (hard) 困难 难度挑战自己,从而在算法练习中加深对数据结构的认识;
(4) 在练习的过程中可以把那些经典的或自己暂时不明白的问题收藏起来,时不时去回顾这些题目,建立错题库,慢慢地加强思维能力。
(5)最重要的还是要多动手去练,一道题目可能看起来很简单,但真正落到代码上你就会发现怎么会出现各种各样的问题,我们就是需要在问题中去锤炼自己的能力。
另外,准备校招的同学还可以多去去看看牛客网。
由于算法的特殊性,这一节也就不留扩展阅读和思考题了,有志于算法突破的同学可以多去力扣中文版上练习。