面试题1:赋值运算符函数
个人总结:
这道题目除了掌握必要的知识点外,比较注重实现的细节。
考察知识点:
- 重载的概念
- 运算符重载
解答思路:
1、如何写运算符重载函数
2、传入参数和返回值类型:传入参数是否需要const
,返回值是否需要引用
3、包含指针成员变量的拷贝:深拷贝or浅拷贝?
4、如何尽可能保证异常安全?比如申请、释放当前对象的指针成员变量所指示地内存区域
class CMyString { public: CMyString(char *pData = nullptr); CMyString(const CMyString& rhs); ~CMyString(); CMyString& operator=(const CMyString& rhs); private: char *m_pData; }; CMyString& CMyString::operator=(const CMyString& rhs) { if (&rhs == this) return *this; // 避免内存申请异常处理,直接调用拷贝构造函数创建临时对象 CMyString tmp(rhs); // 保存临时对象所分配的内存空间指针 char *p_tempData = tmp.m_pData; // 将临时对象的指针成员变量置为当前对象所对应变量的值 // 借助临时对象的析构,安全地释放当前对象地指针变量所申请的内存 tmp.m_pData = m_pData; // 现在可以将当前对象地指针指向刚刚申请地内存了 m_pData = p_tempData; // 为了连续赋值不出错,必须返回引用 return *this; }
衍生问题:
- C++中,哪些函数能重载?哪些不能重载?
- 重载的原理
- 介绍下虚函数机制
面试题2:实现Singleton模式
个人总结:
对C++11及以后来说,这道题实现非常简单。对之前的C++版本,其实现相对复杂,但也只不过是需要考虑的情景变多了。
考察知识点:
static
关键字的用法- 设计模式
解答思路:
1、提供统一接口获取类对象。因为对象没创建,所以是需要static
函数
2、私有化构造函数,防止被外部访问
3、一般不允许单例类拷贝构造和赋值构造,所以把它们删掉
// 这里给出C++11及以后的做法 // TODO: 需要补充C++11之前的解法 class Singleton { public: // 视实际需求选择返回指针或者引用 static Singleton *GetInstance(); private: Singleton() = default; // 按需实现 ~Singleton() = default; // 按需实现 Singleton(const Singleton& rhs) = delete; // 删除拷贝构造函数 Singleton& operator=(const Singleton& rhs) = delete; // 删除赋值构造函数 }; Singleton* Singleton::GetInstance() { // C++11及以后的版本,保证了局部静态变量的创建是线程安全的 // 局部静态变量有两个特点: // 1. 第一次调用时初始化 // 2. 仅第一次被调用时初始化,后续再调用不会再初始化 static Singleton instance; return &instance; }
衍生问题:
- 说下对
static
关键字的理解 - 成员函数后面加
= default
、= 0
、= delete
有什么含义
面试题3:数组中的重复数字
解答思路:
注意题目条件:
1、数组长度为n
2、每个元素的范围0 ~ n-1
3、有一个至多个数字重复
4、只需要返回任意一个重复数字
应当问清楚的事:
1、能不能修改原数组
2、一定能找到吗?找不到应该如何返回
class Solution { public: int findRepeatNumber(std::vector<int>& nums) { if (nums.empty()) return -1; // 这里定义找不到时返回-1,实际需要与面试官确认 // 解法1:时间O(n),空间O(n) std::unordered_set<int> st; for (const int& n : nums) { if (st.count(n)) return n; st.insert(n); } // 解法2:时间O(n),空间O(1) for (int i = 0; i < nums.size(); ++i) { // 比较当前i与nums[i] // 如果相等,进行下一轮for循环;否则,找到“i” while(nums[i] != i) { // 如果第nums[i]和它所对应的数nums[nums[i]]相等 // 则说明这个数重复,故而返回 if (nums[i] == nums[nums[i]]) return nums[i]; // 否则,将nums[i]放到它应该在的位置 std::swap(nums[i], nums[nums[i]]); } } return -1; } };
衍生问题:
- 在长度为n+1的数组中寻找重复数字,数组中元素的范围1~n
// 可参考上一个解法,用hash map。这里放上另一种解法 class Solution { public: // 以时间换空间:时间:O(nlogn),空间:O(1) int findRepeatNumber(const std::vector<int>& nums) { if (nums.empty()) return -1; int start = 1; int end = nums.size() - 1; while(start <= end) { int mid = start + (end - start) / 2; int cnt = count(nums, start, mid); if (start == end) { if (cnt > 1) return start; else break; } if (cnt > (mid - start + 1)) end = mid; else start = mid + 1; } return -1; } };
- 找出所有重复数字
面试题4:二维数组中的查找
考察知识点:
- 矩阵操作
解答思路:
根据增长特点,选择一个能缩小查找范围的位置进行查找。本题适合从左下角或者右上角入手。
class Solution { public: bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) { if (!matrix.empty()) { const int m = matrix.size(); const int n = matrix[0].size(); int i = 0; int j = n - 1; // 这里从右上角开始查询 while(i < m && j >= 0) { if (matrix[i][j] == target) return true; else if (matrix[i][j] < target) ++i; else --j; } } return false; } };
衍生问题:
- 三维空间中的查找???
面试题5:替换空格
考察知识点:
从后往前遍历
解答思路:
class Solution { public: // Leetcode上将这道题目简化了,实际是在原数组上操作,从后往前遍历才对 string replaceSpace(string s) { string res; for (char& c : s) { if (c == ' ') { res += "%20"; } else { res += c; } } return res; } // void replaceSpace(char str[], int length, int n) { if (str == nullptr) return ; int idx = n - 1; for (int i = length - 1; i >= 0; --i) { if (str[i] == ' ') { str[idx--] = '0'; str[idx--] = '2'; str[idx--] = '%'; } else { str[idx--] = str[i]; } } } };
面试题6:从尾到头打印链表
考察知识点:
- 栈
解答思路:
class Solution { public: vector<int> reversePrint(ListNode* head) { if (head == nullptr) return {}; vector<int> res; stack<ListNode*> st; ListNode* node = head; while(node != nullptr) { st.push(node); node = node->next; } res.resize(st.size()); int idx = 0; while(!st.empty()) { res[idx++] = st.top()->val; st.pop(); } return res; } };
面试题7:重建二叉树
解答思路:
1、已知前序遍历和中序遍历
2、二叉树中不含重复元素
/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode(int x) : val(x), left(NULL), right(NULL) {} * }; */ class Solution { public: TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) { return helper(preorder, 0, preorder.size() - 1, inorder, 0, inorder.size() - 1); } TreeNode* helper(const vector<int>& preorder, int preStart, int preStop, const vector<int>& inorder, int inStart, int inStop) { if (preStart > preStop || inStart > inStop) return nullptr; // 前序遍历第一个数是根节点的值 TreeNode* root = new TreeNode(preorder[preStart]); int idx = inStart; for (idx = inStart; idx <= inStop; ++idx) { // 在中序遍历中找到根节点的index if (inorder[idx] == root->val) break; } // 计算出左子树的长度 int lenOfLeft = idx - inStart + 1; if (lenOfLeft) { // 递归构建左子树 root->left = helper(preorder, preStart + 1, preStart + lenOfLeft - 1, inorder, inStart, idx - 1); } if (lenOfLeft < preStop - preStart) { // 递归构建右子树 root->right = helper(preorder, preStart + lenOfLeft, preStop, inorder, idx + 1, inStop); } return root; } };
面试题8:二叉树的下一个节点
解答思路:
观察题目,注意到条件:
1、子节点包含指向父节点的指针
既然提到了这个,说明它是十分右帮助的
struct BTNode { int val; BTNode* parent; BTNode* left; BTNode* right; BTNode(int _val) : val(_val), parent(nullptr), left(nullptr), right(nullptr) {} BTNode(int _val, BTNode* _p, BTNode* _l, BTNode* _r) : val(_val), parent(_p), left(_l), right(_r) {} }; class Solution { public: BTNode *next(BTNode* root) { if (root == nullptr) return nullptr; BTNode* next = nullptr; // 1. 如果root节点的右孩子存在,则下一个节点是右子树的最左叶节点 if (root->right != nullptr) { BTNode *pRight = root->right; while(pRight->left != nullptr) pRight = pRight->left; next = pRight; // 2. 如果root节点没有右孩子,沿着它的父节点查找 } else if (root->parent != nullptr) { BTNode *cur = root; BTNode *parent = cur->parent; // 3. 如果父节点存在,且当前节点是父节点的右孩子 // 一直向上找到不是父节点右节点的节点 while(parent != nullptr && parent->right == cur) { cur = parent; parent = parent->parent; } next = parent; } return next; } };
面试题9:用两个栈实现队列
解答思路:
1、队列的特点:先进先出
2、栈的特点:先进后出
故而可以这么做:
1、push()
操作时,往一个栈里填数据
2、pop()
操作时,
2.1 首先检查另一个栈是否为空,如果不为空,则直接对这个栈进行pop()
2.2 如果为空,则将第一个栈的数据压入这个栈,再对这个栈执行pop()
class CQueue { public: CQueue() { } void appendTail(int value) { mStackIn.push(value); } int deleteHead() { int ans = -1; // 输出栈不为空,则直接从输出栈弹出数据 if (!mStackOut.empty()) { ans = mStackOut.top(); mStackOut.pop(); } else { // 输出栈为空,把输入栈中的数据“灌”入输出栈 while(!mStackIn.empty()) { mStackOut.push(mStack1.top()); mStackIn.pop(); } // 注意,输入栈同样可能也为空,因此取值前需要对该异常情况检测 // 这一步操作也可挪到函数开头完成 if (!mStackOut.empty()) { ans = mStackOut.top(); mStackOut.pop(); } } return ans; } private: stack<int> mStackIn; // 用于外部数据输入,appendTail()操作只会影响这一个栈 stack<int> mStackOut; // 用于外部数据输出 };
面试题10:斐波那契数列
解答思路:
有几种做法:
1、递归法。此种方法太过简单,此处不赘述
2、动态规划。写出状态转移方程。
注意:
1、这道题目需要仔细确认起始值
2、整型溢出问题
class Solution { public: // Leetcode这里对返回结果有限制,注意整型溢出问题 static constexpr int MOD = 1000000007; int fib(int n) { if (n < 0) return -1; if (n < 2) return n; int fst = 0; int sec = 1; for (int i = 2; i <= n; ++i) { int tmp = sec; sec = (int)((long)(fst + sec) % MOD); fst = tmp; } return sec; } };
衍生问题:
题目二:青蛙跳台阶问题
解答思路:
动态规划问题,最后分析发现本质上仍是一道斐波那契数列问题
class Solution { public: static constexpr int MOD = 1000000007; int numWays(int n) { if (n < 0) return -1; if (n < 2) return 1; int fst = 1; int sec = 1; for (int i = 2; i <= n; ++i) { int tmp = sec; sec = static_cast<int>(static_cast<long>(fst + sec) % MOD); fst = tmp; } return sec; } };
面试题11:旋转数组的最小数字
解答思路:
面试题12:
解答思路:
面试题13:
解答思路:
面试题14:
解答思路:
面试题15:
解答思路:
面试题16:
解答思路:
面试题17:
解答思路:
面试题18:
解答思路:
面试题19:
解答思路:
面试题20:
解答思路: