数据结构面试常见问题(三)

作为程序员,数据结构无疑是最基本也是最重要的知识点之一。在面试中,面试官也经常会问及与数据结构相关的问题,以考察应聘者的编程基础知识。本文将介绍几个常见的面试问题,并给出解答思路和代码示例。

数据结构面试常见问题

1. 反转链表

老规矩还是先来个经典例题!反转链表是链表操作中一个非常典型的问题,它的考察点在于对链表指针操作的熟练程度。

思路:

  1. 定义三个指针变量 prevcurrnext,初始时 prevnullcurr 指向头节点、next 指向 curr 的下一个节点。
  2. 循环遍历链表,每次循环执行以下操作:
    • currnext 指针指向 prev
    • prev 指针后移
    • curr 指针后移
  3. 循环结束后,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. 合并两个有序链表

合并两个有序链表也是一个非常常见的面试题,考察的重点在于能否理解归并排序的思想,并加以实现。

思路:

  1. 创建一个新链表,新链表有一个 dummy 节点作为头节点,用于简化操作。
  2. 定义一个 curr 指针,初始指向 dummy 节点。
  3. 循环遍历两个链表,每次取较小值作为新链表的下一个节点。
  4. 如果有一个链表遍历完了,则将剩余的另一个链表直接拼接到新链表的尾部即可。

代码(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,判断字符串是否有效。括号必须以正确的顺序闭合。

思路:

  1. 创建一个栈结构,遍历字符串 s
  2. 对于左括号字符,将其压入栈中。
  3. 对于右括号字符,从栈顶取出最后一个左括号字符,判断是否与当前右括号匹配。
    • 如果匹配,继续遍历。
    • 如果不匹配或栈为空,则返回 false
  4. 遍历完成后,如果栈为空,说明所有括号全部匹配,返回 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. 二叉树的层序遍历

给定一个二叉树的根节点,返回其按层级遍历的节点值(即从左到右,逐层遍历)。

思路:

  1. 创建一个队列,初始化时将根节点入队。
  2. 循环遍历队列:
    • 记录当前层级节点个数 size
    • 循环 size 次,每次从队列出队一个节点,将其左右子节点入队
    • 将出队节点的值添加到当前层级的结果数组
  3. 返回最终的遍历结果数组

代码(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,请判断它是否是回文串(从前往后和从后往前读都是相同的字符串)。忽略字母的大小写。

思路:

  1. 将字符串 s 转换为小写,并且只保留字母和数字字符。
  2. 创建两个指针 leftright,分别指向字符串的首尾。
  3. 循环遍历:
    • 如果 leftright 指向的字符不相等,则返回 false
    • 否则,left 右移,right 左移。
  4. 循环结束后,没有返回 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,找出数组中两个数的下标,使得这两个数相加之和等于目标值。

思路:

  1. 创建一个哈希表 map,用于存储已经遍历过的数字和其对应的下标。
  2. 遍历数组 nums:
    • 计算目标值与当前数字的差值 need
    • 判断 map 中是否存在 need:
      • 存在,则返回 need 的下标和当前下标
      • 不存在,则将当前数字和下标存入 map
  3. 遍历完成没有找到结果,则返回空数组

代码(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. 相交链表

给定两个单链表的头节点 headAheadB,请找出两个单链表相交的起始节点。如果没有相交节点,返回 null

思路:

  1. 计算链表 A 和链表 B 的长度,得到两个长度差值 diff
  2. 让较长链表先走 diff 步。
  3. 然后两个链表同时遍历,直到节点相同则返回该节点,否则返回 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,请合并所有重叠的区间。

思路:

  1. 先按区间的左边界进行排序。
  2. 遍历排序后的区间集合:
    • 初始化一个新区间 newInterval,等于第一个区间。
    • 对于后续的每个区间:
      • 如果该区间的左边界小于等于 newInterval 的右边界,则更新 newInterval 的右边界为该区间和 newInterval 右边界的最大值。
      • 否则,将 newInterval 加入结果数组,并更新 newInterval 为当前区间。
  3. 遍历结束,将最后一个 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,找到一个具有最大和的连续子数组(至少包含一个数字),返回其最大和。

思路:

  1. 创建一个变量 maxSum 来存储最大子序和,初始化为数组的第一个元素。
  2. 创建一个变量 currSum 来存储当前的子序和,初始化为数组的第一个元素。
  3. 从第二个元素开始遍历数组:
    • 如果 currSum 大于 0,则将当前元素值加到 currSum
    • 如果 currSum 小于等于 0,则舍弃之前的子序和,重新计算子序和。
    • 更新 maxSum 为当前 currSummaxSum 中的最大值。
  4. 遍历结束后,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,反转这棵二叉树。

思路:

  1. 创建一个递归函数 invertTree,接收当前节点 node 作为参数。
  2. 如果 node 为空,则直接返回。
  3. 否则,交换左右子节点的位置:
    • 先交换当前节点的左右子节点
    • 再递归处理左子节点和右子节点
  4. 最后返回处理后的节点即可。

代码(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; // 返回反转后的根节点
}

总结

以上是一些常见的数据结构相关的面试题目,涉及了链表、树、字符串、数组等不同数据结构,以及一些基本的算法思想。在面试中,不仅要掌握这些基础知识,同时也要具备良好的编码能力、逻辑思维能力,能够正确高效地实现出解题思路。

此外,数据结构和算法作为计算机科学的基石,其重要性不言而喻。无论是在面试中,还是在日常的编程工作中,熟练掌握这些核心知识都是至关重要的。我们应该不断学习、思考、实践,持续提升自己的编程能力,以更好地应对未来的挑战。

下一篇我们继续前进!!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码侠At哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值