C++八股文(数据结构与算法)

1. 如何实现一个链表?

单链表核心实现

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class LinkedList {
    ListNode* head;
public:
    LinkedList() : head(nullptr) {}
    
    void insert(int val) {  // 头插
        ListNode* node = new ListNode(val);
        node->next = head;
        head = node;
    }
    
    void remove(int val) {  // 删除节点
        ListNode dummy(0);
        dummy.next = head;
        ListNode* prev = &dummy;
        while (prev->next) {
            if (prev->next->val == val) {
                ListNode* temp = prev->next;
                prev->next = prev->next->next;
                delete temp;
                break;
            }
            prev = prev->next;
        }
        head = dummy.next;
    }
};

  • 关键点:节点结构(数据+指针)、头指针管理、插入删除操作
  • 双向链表:节点增加prev指针,可双向遍历
  • 循环链表:尾节点指向头节点
  • 常见操作:反转链表、查找中间节点、合并有序链表、检测环
  • 优点:动态大小、插入删除O(1)
  • 缺点:随机访问O(n)、额外指针空间

2. 如何实现栈和队列?

栈(数组实现)

class Stack {
    vector<int> data;
public:
    void push(int x) { data.push_back(x); }
    void pop() { if (!empty()) data.pop_back(); }
    int top() { return data.back(); }
    bool empty() { return data.empty(); }
};

队列(链表实现)

class Queue {
    list<int> data;
public:
    void push(int x) { data.push_back(x); }
    void pop() { if (!empty()) data.pop_front(); }
    int front() { return data.front(); }
    bool empty() { return data.empty(); }
};

  • 栈特点:LIFO(后进先出),用于函数调用、表达式求值、DFS
  • 队列特点:FIFO(先进先出),用于BFS、任务调度
  • 循环队列:数组实现,避免空间浪费,需要front、rear指针
  • 双端队列:两端都可插入删除,STL的deque
  • STL容器:stack、queue、priority_queue(堆)

3. 如何实现二叉树的前序、中序、后序遍历?

struct TreeNode {
    int val;
    TreeNode *left, *right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// 前序:根-左-右
void preorder(TreeNode* root) {
    if (!root) return;
    cout << root->val << " ";
    preorder(root->left);
    preorder(root->right);
}

// 中序:左-根-右(二叉搜索树得到有序序列)
void inorder(TreeNode* root) {
    if (!root) return;
    inorder(root->left);
    cout << root->val << " ";
    inorder(root->right);
}

// 后序:左-右-根
void postorder(TreeNode* root) {
    if (!root) return;
    postorder(root->left);
    postorder(root->right);
    cout << root->val << " ";
}

// 层序遍历(BFS)
void levelorder(TreeNode* root) {
    if (!root) return;
    queue<TreeNode*> q;
    q.push(root);
    while (!q.empty()) {
        TreeNode* node = q.front();
        q.pop();
        cout << node->val << " ";
        if (node->left) q.push(node->left);
        if (node->right) q.push(node->right);
    }
}

  • 递归实现:简洁但可能栈溢出
  • 迭代实现:用栈模拟递归,前序和中序较简单,后序稍复杂
  • Morris遍历:O(1)空间,利用线索二叉树
  • 应用:前序用于复制树、中序用于BST排序、后序用于删除树

4. 如何实现哈希表?

class HashTable {
    static const int SIZE = 1000;
    vector<list<pair<int, int>>> table;  // 链地址法
public:
    HashTable() : table(SIZE) {}
    
    int hash(int key) { return key % SIZE; }
    
    void insert(int key, int value) {
        int idx = hash(key);
        for (auto& p : table[idx]) {
            if (p.first == key) {
                p.second = value;  // 更新
                return;
            }
        }
        table[idx].push_back({key, value});
    }
    
    int get(int key) {
        int idx = hash(key);
        for (auto& p : table[idx]) {
            if (p.first == key) return p.second;
        }
        return -1;  // 不存在
    }
    
    void remove(int key) {
        int idx = hash(key);
        table[idx].remove_if([key](auto& p) { return p.first == key; });
    }
};

  • 哈希函数:将key映射到索引,要求分布均匀、计算快速
  • 冲突解决:①链地址法(拉链法)②开放地址法(线性探测、二次探测、双重哈希)
  • 负载因子:元素数/桶数,过高需要rehash扩容
  • 时间复杂度:平均O(1),最坏O(n)
  • STL实现:unordered_map、unordered_set

5. 如何使用 std::map 和 std::unordered_map?

// map:红黑树实现,有序,O(log n)
map<string, int> m;
m["apple"] = 1;
m.insert({"banana", 2});
if (m.count("apple")) cout << m["apple"];
for (auto& [key, val] : m) cout << key << ":" << val;

// unordered_map:哈希表实现,无序,O(1)
unordered_map<string, int> um;
um["apple"] = 1;
auto it = um.find("apple");
if (it != um.end()) cout << it->second;
um.erase("apple");

  • map特点:有序(按key排序)、O(log n)操作、迭代器稳定
  • unordered_map特点:无序、平均O(1)、可能rehash导致迭代器失效
  • 选择依据:需要有序用map,追求性能用unordered_map
  • 自定义类型:map需要<运算符,unordered_map需要hash函数和==运算符
  • 常用操作:insert、erase、find、count、[]运算符(不存在会插入)

6. 如何实现图的DFS和BFS?

// 邻接表表示图
vector<vector<int>> graph;

// DFS递归
void dfs(int node, vector<bool>& visited) {
    visited[node] = true;
    cout << node << " ";
    for (int neighbor : graph[node]) {
        if (!visited[neighbor]) {
            dfs(neighbor, visited);
        }
    }
}

// DFS迭代(栈)
void dfsIterative(int start) {
    vector<bool> visited(graph.size(), false);
    stack<int> s;
    s.push(start);
    while (!s.empty()) {
        int node = s.top();
        s.pop();
        if (visited[node]) continue;
        visited[node] = true;
        cout << node << " ";
        for (int neighbor : graph[node]) {
            if (!visited[neighbor]) s.push(neighbor);
        }
    }
}

// BFS(队列)
void bfs(int start) {
    vector<bool> visited(graph.size(), false);
    queue<int> q;
    q.push(start);
    visited[start] = true;
    while (!q.empty()) {
        int node = q.front();
        q.pop();
        cout << node << " ";
        for (int neighbor : graph[node]) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                q.push(neighbor);
            }
        }
    }
}

  • DFS特点:深度优先,用栈(递归或显式),适合路径搜索、拓扑排序
  • BFS特点:广度优先,用队列,适合最短路径、层次遍历
  • 时间复杂度:O(V+E),V顶点数,E边数
  • 应用:连通性检测、环检测、最短路径、拓扑排序

7. 如何使用 std::vector 和 std::array?

// vector:动态数组
vector<int> v = {1, 2, 3};
v.push_back(4);
v.pop_back();
v.insert(v.begin() + 1, 10);  // 位置1插入10
v.erase(v.begin());
cout << v.size() << " " << v.capacity();
v.reserve(100);  // 预分配容量
v.shrink_to_fit();  // 释放多余容量

// array:固定大小数组(C++11)
array<int, 5> arr = {1, 2, 3, 4, 5};
arr[0] = 10;
arr.at(1) = 20;  // 带边界检查
for (int x : arr) cout << x;

  • vector特点:动态大小、连续内存、随机访问O(1)、尾部插入删除O(1)、中间操作O(n)
  • 扩容机制:容量不足时重新分配(通常2倍),拷贝元素,旧内存释放
  • array特点:编译期固定大小、栈分配、无动态分配开销、不能改变大小
  • 选择:大小固定用array,动态变化用vector
  • 常用操作:push_back、emplace_back(原地构造)、resize、clear、swap

8. 如何判断一个字符串是回文?

// 双指针法
bool isPalindrome(string s) {
    int left = 0, right = s.length() - 1;
    while (left < right) {
        if (s[left] != s[right]) return false;
        left++;
        right--;
    }
    return true;
}

// 忽略非字母数字、忽略大小写
bool isPalindromeAlphanumeric(string s) {
    int left = 0, right = s.length() - 1;
    while (left < right) {
        while (left < right && !isalnum(s[left])) left++;
        while (left < right && !isalnum(s[right])) right--;
        if (tolower(s[left]) != tolower(s[right])) return false;
        left++;
        right--;
    }
    return true;
}

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 变体:最长回文子串(动态规划或中心扩展)、回文链表(快慢指针+反转)
  • 技巧:双指针、中心扩展、Manacher算法(O(n)找最长回文)

9. 如何实现快速排序和归并排序?

// 快速排序:分治,选pivot,小的左边大的右边
void quickSort(vector<int>& arr, int left, int right) {
    if (left >= right) return;
    int pivot = arr[left + (right - left) / 2];
    int i = left, j = right;
    while (i <= j) {
        while (arr[i] < pivot) i++;
        while (arr[j] > pivot) j--;
        if (i <= j) {
            swap(arr[i], arr[j]);
            i++;
            j--;
        }
    }
    quickSort(arr, left, j);
    quickSort(arr, i, right);
}

// 归并排序:分治,递归排序左右,合并
void merge(vector<int>& arr, int left, int mid, int right) {
    vector<int> temp(right - left + 1);
    int i = left, j = mid + 1, k = 0;
    while (i <= mid && j <= right) {
        temp[k++] = arr[i] < arr[j] ? arr[i++] : arr[j++];
    }
    while (i <= mid) temp[k++] = arr[i++];
    while (j <= right) temp[k++] = arr[j++];
    for (int i = 0; i < temp.size(); i++) {
        arr[left + i] = temp[i];
    }
}

void mergeSort(vector<int>& arr, int left, int right) {
    if (left >= right) return;
    int mid = left + (right - left) / 2;
    mergeSort(arr, left, mid);
    mergeSort(arr, mid + 1, right);
    merge(arr, left, mid, right);
}

  • 快速排序:平均O(n log n),最坏O(n²),不稳定,原地排序
  • 归并排序:O(n log n),稳定,需要O(n)额外空间
  • 优化:快排选择随机pivot、三数取中;小数组用插入排序
  • STL:std::sort(快排+堆排+插入排序混合)、std::stable_sort(归并排序)

10. 如何判断一个链表是否有环?

// Floyd判圈算法(快慢指针)
bool hasCycle(ListNode* head) {
    if (!head) return false;
    ListNode *slow = head, *fast = head;
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) return true;  // 相遇说明有环
    }
    return false;
}

// 找环的入口
ListNode* detectCycle(ListNode* head) {
    ListNode *slow = head, *fast = head;
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) {
            // 相遇后,一个指针回到头,两个都走一步,再次相遇就是入口
            ListNode* ptr = head;
            while (ptr != slow) {
                ptr = ptr->next;
                slow = slow->next;
            }
            return ptr;
        }
    }
    return nullptr;
}

  • 快慢指针原理:有环则快指针会追上慢指针,无环则快指针到达末尾
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 数学证明:设环外长度a,环长b,相遇时慢指针走a+x,快指针走a+nb+x,2(a+x)=a+nb+x,得a=nb-x
  • 其他方法:哈希表记录访问过的节点(O(n)空间)

C++八股文全集 文章被收录于专栏

本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

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