作为程序员,数据结构无疑是最基本也是最重要的知识点之一。在面试中,面试官也经常会问及与数据结构相关的问题,以考察应聘者的编程基础知识。本文将介绍几个常见的面试问题,并给出解答思路和代码示例。
1. 反转链表
老规矩还是先来个经典例题!反转链表是链表操作中一个非常典型的问题,它的考察点在于对链表指针操作的熟练程度。
思路:
- 定义三个指针变量
prev
、curr
、next
,初始时prev
为null
、curr
指向头节点、next
指向curr
的下一个节点。 - 循环遍历链表,每次循环执行以下操作:
- 将
curr
的next
指针指向prev
prev
指针后移curr
指针后移
- 将
- 循环结束后,
prev
指向反转后的头节点
代码(JavaScript):
/**
* 反转链表
* @param {ListNode} head
* @returns {ListNode} 反转后的链表头
*/
function reverseList(head) {
let prev = null, curr = head, next = null;
while(curr) {
next = curr.next; // 暂存后继节点
curr.next = prev; // 修改当前节点指针
prev = curr; // 前驱指针后移
curr = next; // 当前指针后移
}
return prev; // 返回反转后的头节点
}
2. 合并两个有序链表
合并两个有序链表也是一个非常常见的面试题,考察的重点在于能否理解归并排序的思想,并加以实现。
思路:
- 创建一个新链表,新链表有一个
dummy
节点作为头节点,用于简化操作。 - 定义一个
curr
指针,初始指向dummy
节点。 - 循环遍历两个链表,每次取较小值作为新链表的下一个节点。
- 如果有一个链表遍历完了,则将剩余的另一个链表直接拼接到新链表的尾部即可。
代码(JavaScript):
/**
* 合并两个有序链表
* @param {ListNode} list1
* @param {ListNode} list2
* @returns {ListNode} 合并后的链表头
*/
function mergeTwoLists(list1, list2) {
let dummy = new ListNode(); // 创建虚拟头节点
let curr = dummy; // curr 作为遍历指针
while(list1 && list2) {
if(list1.val <= list2.val) {
curr.next = list1;
list1 = list1.next;
} else {
curr.next = list2;
list2 = list2.next;
}
curr = curr.next; // 遍历指针后移
}
// 将剩余的链表直接拼接到新链表后面
curr.next = list1 || list2;
return dummy.next; // 返回新链表的头节点
}
3. 有效的括号
给定一个只包括 (
、)
、{
、}
、[
、]
的字符串 s
,判断字符串是否有效。括号必须以正确的顺序闭合。
思路:
- 创建一个栈结构,遍历字符串
s
。 - 对于左括号字符,将其压入栈中。
- 对于右括号字符,从栈顶取出最后一个左括号字符,判断是否与当前右括号匹配。
- 如果匹配,继续遍历。
- 如果不匹配或栈为空,则返回
false
。
- 遍历完成后,如果栈为空,说明所有括号全部匹配,返回
true
,否则返回false
。
代码(JavaScript):
/**
* 判断括号是否有效
* @param {string} s
* @return {boolean}
*/
function isValid(s) {
const stack = [];
const brackets = {
'(': ')',
'[': ']',
'{': '}'
};
for(let char of s) {
if(brackets[char]) { // 左括号,入栈
stack.push(char);
} else { // 右括号
const topChar = stack.pop(); // 取出栈顶左括号
if(!topChar || brackets[topChar] !== char) { // 判断是否匹配
return false;
}
}
}
return stack.length === 0; // 如果栈为空,则说明括号全部匹配
}
以上是三个常见的面试问题,涉及了链表操作和栈等基本数据结构的应用。在面试中,掌握这些基础十分重要,同时也要具备扎实的编码能力,能够正确高效地实现出解题思路。
恭喜你完成了第一阶段的任务!接下来我们继续。
4. 二叉树的层序遍历
给定一个二叉树的根节点,返回其按层级遍历的节点值(即从左到右,逐层遍历)。
思路:
- 创建一个队列,初始化时将根节点入队。
- 循环遍历队列:
- 记录当前层级节点个数
size
- 循环
size
次,每次从队列出队一个节点,将其左右子节点入队 - 将出队节点的值添加到当前层级的结果数组
- 记录当前层级节点个数
- 返回最终的遍历结果数组
代码(JavaScript):
/**
* 二叉树的层序遍历
* @param {TreeNode} root
* @return {number[][]}
*/
function levelOrder(root) {
if(!root) return [];
const queue = [root], res = [];
while(queue.length) {
const size = queue.length; // 当前层级节点个数
const level = []; // 存储当前层级节点值的数组
for(let i = 0; i < size; i++) {
const node = queue.shift(); // 出队
level.push(node.val); // 存储该节点值
if(node.left) queue.push(node.left); // 左子节点入队
if(node.right) queue.push(node.right); // 右子节点入队
}
res.push(level); // 存储当前层级遍历结果
}
return res;
}
5. 有效的回文串
给定一个字符串 s
,请判断它是否是回文串(从前往后和从后往前读都是相同的字符串)。忽略字母的大小写。
思路:
- 将字符串
s
转换为小写,并且只保留字母和数字字符。 - 创建两个指针
left
和right
,分别指向字符串的首尾。 - 循环遍历:
- 如果
left
和right
指向的字符不相等,则返回false
。 - 否则,
left
右移,right
左移。
- 如果
- 循环结束后,没有返回
false
,说明是回文串,返回true
。
代码(JavaScript):
/**
* 判断是否回文串
* @param {string} s
* @return {boolean}
*/
function isPalindrome(s) {
const str = s.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); // 预处理字符串
let left = 0, right = str.length - 1;
while(left < right) {
if(str[left] !== str[right]) return false;
left++;
right--;
}
return true;
}
6. 两数之和
给定一个整数数组 nums
和一个目标值 target
,找出数组中两个数的下标,使得这两个数相加之和等于目标值。
思路:
- 创建一个哈希表
map
,用于存储已经遍历过的数字和其对应的下标。 - 遍历数组
nums
:- 计算目标值与当前数字的差值
need
- 判断
map
中是否存在need
:- 存在,则返回
need
的下标和当前下标 - 不存在,则将当前数字和下标存入
map
- 存在,则返回
- 计算目标值与当前数字的差值
- 遍历完成没有找到结果,则返回空数组
代码(JavaScript):
/**
* 两数之和
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
function twoSum(nums, target) {
const map = new Map();
for(let i = 0; i < nums.length; i++) {
const need = target - nums[i];
if(map.has(need)) {
return [map.get(need), i];
} else {
map.set(nums[i], i);
}
}
return [];
}
7. 相交链表
给定两个单链表的头节点 headA
和 headB
,请找出两个单链表相交的起始节点。如果没有相交节点,返回 null
。
思路:
- 计算链表 A 和链表 B 的长度,得到两个长度差值
diff
。 - 让较长链表先走
diff
步。 - 然后两个链表同时遍历,直到节点相同则返回该节点,否则返回
null
。
代码(JavaScript):
/**
* 相交链表
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
function getIntersectionNode(headA, headB) {
let ptrA = headA, ptrB = headB;
let lenA = 0, lenB = 0;
// 计算两个链表的长度
while(ptrA) {
lenA++;
ptrA = ptrA.next;
}
while(ptrB) {
lenB++;
ptrB = ptrB.next;
}
// 重置指针到链表头部
ptrA = headA;
ptrB = headB;
// 让较长链表先走差值步数
if(lenA > lenB) {
for(let i = 0; i < lenA - lenB; i++) {
ptrA = ptrA.next;
}
} else {
for(let i = 0; i < lenB - lenA; i++) {
ptrB = ptrB.next;
}
}
// 同时遍历两链表,直到节点相同
while(ptrA && ptrB) {
if(ptrA === ptrB) {
return ptrA;
}
ptrA = ptrA.next;
ptrB = ptrB.next;
}
return null; // 没有相交节点
}
8. 合并区间
给定一个区间的集合 intervals
,请合并所有重叠的区间。
思路:
- 先按区间的左边界进行排序。
- 遍历排序后的区间集合:
- 初始化一个新区间
newInterval
,等于第一个区间。 - 对于后续的每个区间:
- 如果该区间的左边界小于等于
newInterval
的右边界,则更新newInterval
的右边界为该区间和newInterval
右边界的最大值。 - 否则,将
newInterval
加入结果数组,并更新newInterval
为当前区间。
- 如果该区间的左边界小于等于
- 初始化一个新区间
- 遍历结束,将最后一个
newInterval
加入结果数组即可。
代码(JavaScript):
/**
* 合并重叠区间
* @param {number[][]} intervals
* @return {number[][]}
*/
function merge(intervals) {
if(!intervals.length) return [];
// 按区间左边界排序
intervals.sort((a, b) => a[0] - b[0]);
const res = [];
let newInterval = intervals[0]; // 初始化新区间
for(let i = 1; i < intervals.length; i++) {
const interval = intervals[i];
// 重叠区间,更新新区间
if(interval[0] <= newInterval[1]) {
newInterval[1] = Math.max(newInterval[1], interval[1]);
} else { // 不重叠,合并新区间并更新
res.push(newInterval);
newInterval = interval;
}
}
// 合并最后一个新区间
res.push(newInterval);
return res;
}
9. 最大子序和
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(至少包含一个数字),返回其最大和。
思路:
- 创建一个变量
maxSum
来存储最大子序和,初始化为数组的第一个元素。 - 创建一个变量
currSum
来存储当前的子序和,初始化为数组的第一个元素。 - 从第二个元素开始遍历数组:
- 如果
currSum
大于 0,则将当前元素值加到currSum
。 - 如果
currSum
小于等于 0,则舍弃之前的子序和,重新计算子序和。 - 更新
maxSum
为当前currSum
和maxSum
中的最大值。
- 如果
- 遍历结束后,
maxSum
即为最大子序和。
代码(JavaScript):
/**
* 最大子序和
* @param {number[]} nums
* @return {number}
*/
function maxSubArray(nums) {
let maxSum = nums[0], currSum = nums[0];
for(let i = 1; i < nums.length; i++) {
currSum = Math.max(nums[i], currSum + nums[i]);
maxSum = Math.max(maxSum, currSum);
}
return maxSum;
}
10. 反转二叉树
给定一棵二叉树的根节点 root
,反转这棵二叉树。
思路:
- 创建一个递归函数
invertTree
,接收当前节点node
作为参数。 - 如果
node
为空,则直接返回。 - 否则,交换左右子节点的位置:
- 先交换当前节点的左右子节点
- 再递归处理左子节点和右子节点
- 最后返回处理后的节点即可。
代码(JavaScript):
/**
* 反转二叉树
* @param {TreeNode} root
* @return {TreeNode}
*/
function invertTree(root) {
// 递归终止条件
if(!root) return null;
// 交换左右子节点
const temp = root.left;
root.left = root.right;
root.right = temp;
// 递归处理左右子树
root.left = invertTree(root.left);
root.right = invertTree(root.right);
return root; // 返回反转后的根节点
}
总结
以上是一些常见的数据结构相关的面试题目,涉及了链表、树、字符串、数组等不同数据结构,以及一些基本的算法思想。在面试中,不仅要掌握这些基础知识,同时也要具备良好的编码能力、逻辑思维能力,能够正确高效地实现出解题思路。
此外,数据结构和算法作为计算机科学的基石,其重要性不言而喻。无论是在面试中,还是在日常的编程工作中,熟练掌握这些核心知识都是至关重要的。我们应该不断学习、思考、实践,持续提升自己的编程能力,以更好地应对未来的挑战。
下一篇我们继续前进!!!