C++ 数据结构与算法 常考面试题(一)

1. 如何实现快速排序?快排优化有哪些?

核心思想

快速排序基于分治思想,核心逻辑是:选择一个基准值,将数组划分为「小于基准」和「大于基准」的两个子数组,递归对两个子数组重复该过程,直至子数组长度为1(天然有序)。快排的性能核心取决于基准值的选择和分区效率。

代码实现(经典Hoare分区法)

#include <vector>
#include <algorithm>
using namespace std;

// 分区函数:返回基准值最终位置
int partition(vector<int>& nums, int left, int right) {
    // 选左边界为基准值
    int pivot = nums[left];
    int i = left, j = right;
    while (i < j) {
        // 右指针找小于基准的数
        while (i < j && nums[j] >= pivot) j--;
        // 左指针找大于基准的数
        while (i < j && nums[i] <= pivot) i++;
        // 交换不符合分区规则的元素
        if (i < j) swap(nums[i], nums[j]);
    }
    // 基准值归位(放到分区中间)
    swap(nums[left], nums[i]);
    return i;
}

// 快速排序主函数
void quickSort(vector<int>& nums, int left, int right) {
    // 递归终止条件:子数组长度≤1
    if (left >= right) return;
    int pivotIdx = partition(nums, left, right);
    // 递归排序左、右子数组
    quickSort(nums, left, pivotIdx - 1);
    quickSort(nums, pivotIdx + 1, right);
}

快排经典优化手段

  1. 基准值优化:三数取中法(取左、中、右边界的中间值作为基准),避免有序数组导致的O(n²)最坏情况。
  2. 小区间优化:当子数组长度小于10时,改用插入排序(减少递归调用的栈开销)。
  3. 尾递归优化:对较长的子数组递归,较短的子数组用循环处理,降低栈帧消耗。
  4. 去重优化:三路快排(将等于基准值的元素聚集到中间),避免重复元素的无效排序。

时间/空间复杂度

  • 平均时间复杂度:O(nlogn)(面试必背)
  • 最坏时间复杂度:O(n²)(优化后可规避)
  • 空间复杂度:O(logn)(递归栈开销,最坏O(n))

2. 如何解决二叉树的层序遍历问题?变体如何应对?

核心思想

二叉树层序遍历基于广度优先搜索(BFS),核心是利用队列记录每一层的节点,通过「记录当前层节点数」实现按层遍历,而非单纯的广度遍历。

代码实现(基础层序遍历,按行输出)

#include <vector>
#include <queue>
using namespace std;

// 二叉树节点定义
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// 层序遍历:返回二维数组,每行对应二叉树的一层
vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int>> res;
    if (!root) return res; // 空树直接返回
    
    queue<TreeNode*> q;
    q.push(root);
    
    while (!q.empty()) {
        int levelSize = q.size(); // 记录当前层的节点数
        vector<int> curLevel;
        
        // 遍历当前层所有节点
        for (int i = 0; i < levelSize; i++) {
            TreeNode* node = q.front();
            q.pop();
            curLevel.push_back(node->val);
            
            // 子节点入队,为下一层遍历做准备
            if (node->left) q.push(node->left);
            if (node->right) q.push(node->right);
        }
        res.push_back(curLevel);
    }
    return res;
}

经典变体及应对技巧

时间/空间复杂度

  • 时间复杂度:O(n)(每个节点仅访问一次)
  • 空间复杂度:O(n)(队列最多存储一层节点,最坏为满二叉树的最后一层)

3. 如何实现哈希表?解决哈希冲突的方法有哪些?

核心思想

哈希表(散列表)的核心是通过哈希函数将键(key)映射到数组的指定索引,实现O(1)平均时间复杂度的增删改查;核心解决的问题是「哈希冲突」(不同key映射到同一索引)。

代码实现(链地址法解决冲突,工业界主流)

#include <vector>
#include <string>
#include <stdexcept>
using namespace std;

// 哈希表节点结构
template <typename K, typename V>
struct HashNode {
    K key;
    V val;
    HashNode* next;
    HashNode(K k, V v) : key(k), val(v), next(nullptr) {}
};

// 简易哈希表(链地址法)
template <typename K, typename V>
class HashMap {
private:
    vector<HashNode<K, V>*> table; // 底层数组
    int size;                       // 实际存储节点数
    const int DEFAULT_CAP = 16;     // 默认初始容量
    const float LOAD_FACTOR = 0.75; // 负载因子阈值(超过则扩容)

    // 简易哈希函数(针对int键,可扩展至其他类型)
    int hash(K key) {
        return abs((int)key) % table.size();
    }

    // 扩容:容量翻倍,重新哈希所有节点
    void resize() {
        int newCap = table.size() * 2;
        vector<HashNode<K, V>*> newTable(newCap, nullptr);
        
        for (auto& node : table) {
            HashNode<K, V>* cur = node;
            while (cur) {
                HashNode<K, V>* next = cur->next;
                int idx = abs((int)cur->key) % newCap;
                cur->next = newTable[idx];
                newTable[idx] = cur;
                cur = next;
            }
        }
        table.swap(newTable);
    }

public:
    HashMap() : table(DEFAULT_CAP, nullptr), size(0) {}

    // 插入/更新键值对
    void put(K key, V val) {
        if ((float)size / table.size() >= LOAD_FACTOR) resize();
        int idx = hash(key);
        HashNode<K, V>* cur = table[idx];
        
        // 已存在该key,更新值
        while (cur) {
            if (cur->key == key) {
                cur->val = val;
                return;
            }
            cur = cur->next;
        }
        
        // 不存在,头插法新增节点
        HashNode<K, V>* newNode = new HashNode<K, V>(key, val);
        newNode->next = table[idx];
        table[idx] = newNode;
        size++;
    }

    // 根据key查找值
    V get(K key) {
        int idx = hash(key);
        HashNode<K, V>* cur = table[idx];
        while (cur) {
            if (cur->key == key) return cur->val;
            cur = cur->next;
        }
        throw out_of_range("key not found");
    }

    // 删除指定key
    void remove(K key) {
        int idx = hash(key);
        HashNode<K, V>* cur = table[idx];
        HashNode<K, V>* pre = nullptr;
        
        while (cur) {
            if (cur->key == key) {
                if (pre) pre->next = cur->next;
                else table[idx] = cur->next;
                delete cur;
                size--;
                return;
            }
            pre = cur;
            cur = cur->next;
        }
    }

    ~HashMap() {
        // 释放所有节点内存
        for (auto& node : table) {
            HashNode<K, V>* cur = node;
            while (cur) {
                HashNode<K, V>* next = cur->next;
                delete cur;
                cur = next;
            }
        }
    }
};

解决哈希冲突的常用方法

  1. 链地址法:本文实现方式,数组每个索引挂链表,冲突节点加入链表(Java HashMap、C++ unordered_map底层)。
  2. 开放定址法:冲突后按规则(线性探测、二次探测)寻找下一个空位置,缺点是易产生聚集。
  3. 再哈希法:冲突后用另一个哈希函数计算新索引,缺点是增加计算开销。
  4. 公共溢出区法:冲突节点全部放入溢出区,适合冲突较少的场景。

4. 如何解决拓扑排序问题?Kahn 算法与 DFS 实现?

核心思想

拓扑排序针对有向无环图(DAG),核心是将图中所有节点排成一个线性序列,使得任意有向边u→v中,u都出现在v之前;常用来解决「依赖关系」问题(如课程安排、任务调度)。

方法1:Kahn算法(基于入度的BFS实现)

核心逻辑

  1. 计算所有节点的入度;
  2. 将入度为0的节点入队(无前置依赖);
  3. 出队节点并加入结果,将其邻接节点入度减1;
  4. 重复步骤2-3,直至队空;
  5. 若结果长度≠节点总数,说明图有环,无拓扑序。
#include <vector>
#include <queue>
using namespace std;

// 拓扑排序(Kahn算法):返回拓扑序列,空数组表示有环
vector<int> topologicalSortKahn(int n, vector<vector<int>>& edges) {
    vector<int> inDegree(n, 0); // 入度数组
    vector<vector<int>> adj(n); // 邻接表
    
    // 构建邻接表+统计入度
    for (auto& edge : edges) {
        int u = edge[0], v = edge[1];
        adj[u].push_back(v);
        inDegree[v]++;
    }
    
    queue<int> q;
    // 入度为0的节点入队
    for (int i = 0; i < n; i++) {
        if (inDegree[i] == 0) q.push(i);
    }
    
    vector<int> res;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        res.push_back(u);
        
        // 遍历邻接节点,入度减1
        for (int v : adj[u]) {
            inDegree[v]--;
            if (inDegree[v] == 0) q.push(v);
        }
    }
    
    // 有环:结果长度≠节点数
    if (res.size() != n) return {};
    return res;
}

方法2:DFS实现(基于后序遍历)

核心逻辑

  1. 递归遍历节点,标记访问状态(未访问/访问中/已访问);
  2. 访问中遇到「访问中」的节点,说明有环;
  3. 后序遍历完成后,将节点加入结果;
  4. 最终反转结果,得到拓扑序。
#include <vector>
#include <algorithm>
using namespace std;

// 辅助DFS函数
bool dfs(int u, vector<vector<int>>& adj, vector<int>& visited, vector<int>& res) {
    visited[u] = 1; // 标记为访问中
    for (int v : adj[u]) {
        if (visited[v] == 1) return false; // 发现环
        if (visi

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++ 常考面试题总结 文章被收录于专栏

本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

全部评论
没有后续了吗
点赞 回复 分享
发布于 昨天 21:42 云南
收藏了
点赞 回复 分享
发布于 昨天 21:28 北京

相关推荐

评论
2
2
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务