数组
- 空间效率不好,时间效率好。
- 动态数组要尽量减少改变数组容量大小的次数。
- 数组的名字也是一个指针,该指针指向数组的第一个元素。
- 数组作为函数的参数进行传递时,数组就自动退化为同类型的指针。
4、二维数组中的查找
1、暴力查找:循环遍历,O(n2)
2、利用递增的规律,右上或左下查找:当最右上的数字大于查找值时,将此列删除;如果小于查找值时,将此行删除;如果等于,则查找结束。N=max(m,n), O(N)
class Solution {
public:
bool Find(int target, vector<vector<int> > array) {
// 获取二维数组的行列
int rows = array.size();
int cols = array[0].size();
// if(rows > 0 && cols > 0){
int i = 0;
int j = cols - 1;
while(i < rows && j>=0){
if(array[i][j] == target) return true;
else if(array[i][j] > target) --j;
else ++i;
}
// }
return false;
}
3、二分法:把每一行当成有序一维数组,对每一行进行二分查找。O(mlogn)
class Solution {
public:
bool Find(int target, vector<vector<int> > array) {
// 二分法
for(int n=0; n<array.size(); n++){
int i = 0;
int j = array[n].size() - 1;
while( i<= j){
int mid = (i+j)/2;
if(array[n][mid] == target) return true;
else if(array[n][mid] > target) j = mid - 1;
else i = mid + 1;
}
}
return false;
}
};
字符串
- 每个字符串都是以’\n’作为结尾。
- 为了节省内存,C/C++常把字符串放到单独的一个内存区域。当几个指针赋值给相同的常量字符串时,它们实际会指向相同的内存地址。
5、替换空格
1、暴力解法:直接从前往后遍历替换,时间复杂度O(n2)。不可取。
2、从后往前:先确定总长度L_Ori及空格数N_Space;然后计算出插入后的总长度L_New=L_Ori+2*N_Space;使用双指针,从末尾开始插入,并且将字符后移操作。
class Solution {
public:
void replaceSpace(char *str,int length) {
// 1、统计字符长度L1和空格数S
// 2、建立数组新的长度,为 L1+2S
// 3、从末尾开始寻找空格,如过遇到空格,就替换为%20;否则将字符后移
if(str == NULL) return;
int lengthOri = 0;
int numberSpace = 0;
for(int i=0; str[i] != '\0'; i++){
lengthOri++;
if(str[i] == ' ')
++numberSpace;
}
int lengthNew = lengthOri + 2 * numberSpace;
if(lengthNew > length) return;
char* pStr1 = str + lengthOri;
char* pStr2 = str + lengthNew;
while(pStr1 < pStr2){
if(*pStr1 == ' '){ // 带* 是指向此地址的内容,不带* 是指针地址
*pStr2 --= '0';
*pStr2 --= '2';
*pStr2 --= '%';
}
else
*pStr2 --= *pStr1;
--pStr1;
}
}
};
链表
- 链表是面试时被提及最频繁的数据结构。
- 内存分配不是在创建链表是一次性完成,而是每添加一个节点分配一次内存。
- 空间效率比数组高,时间效率低于数组。
6、从尾到头打印链表
1、辅助栈,先进后出:确定是否要改变原输入数据(切记和面试官共沟通好),本题假设不改变。将链表数据压入到栈,然后再把栈内的数据取出返回到vector;
class Solution {
public:
vector<int> printListFromTailToHead(ListNode* head) {
std::stack<ListNode*> node;
ListNode* pNode = head;
vector<int> result;
while(pNode != NULL){
node.push(pNode);
pNode = pNode->next;
}
while(!node.empty()){
result.push_back(node.top()->val);
node.pop();
}
return result;
}
};
2、使用递归:递归本质是一个栈结构。要实现反转输出,每访问到一个节点时,先递归输出它后面的节点,再输出该节点自身。但是递归不足的是,当链表非常长的时候,就会导致函数调用的层级很深,从而有可能导致函数调用栈溢出,鲁棒性差。
class Solution {
vector<int> result;
public:
vector<int> printListFromTailToHead(ListNode* head) {
if(head != NULL){
if(head->next != NULL){
printListFromTailToHead(head->next);
}
result.push_back(head->val);
}
return result;
}
};
树
- 面试提及的树大部分是二叉树。
- 二叉树是树的一种特殊结构,在二叉树中每个节点最多只能有两个节点。
- 前序遍历:根节点——左子节点——右子节点。
- 中序遍历:左子节点——根节点——右子节点。
- 后序遍历:左子节点——右子节点——根节点。
- 【注:以上都可以用循环和递归实现,递归代码更简单,都要了如指掌】
- 二叉树特例:
- 二叉搜索树:左子节点 <= 根节点 <= 右子节点, 平均O(logn)。
- 堆:
- 最大堆:根节点值最大
- 最小堆:根节点值
- 红黑树:(C++的STL中,set、multiset、map、multimap等)
- 1、当出现新的节点时默认为红色插入,如果其父节点为红色,则对其递归向上换色;如果根节点由此变为红色,则对根节点进行左旋(右侧过深)或右旋(左侧过深);之后从根节点向下修改颜色
- 2、从根节点检查红色节点是否符合路径上的黑色节点数量一致,如果不一致,对该节点进行左旋(右侧黑色节点数量更多)或右旋(左侧黑色节点数量更多),并变换颜色,重复2操作直到符合红黑树规则。
- 3、祖宗根节点必黑色,允许黑连黑,不允许红连红;新增红色,父叔节点通红就变色,父叔黑就旋转,哪黑往哪旋。
7、重建二叉树
1、先求根,再找左右子树,再递归:
class Solution {
public:
TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
// 1、先利用前序pre[0]找到根节点root,并得到vin的长度len
// 2、然后在中序vin中找到root的位置记录为root_vin
// 0 ~ root_vin-1 是中序左子树 ——> 放到 vin_left
// 1 ~ root_vin 是前序左子树 ——> 放到 pre_left
// root_vin+1 ~ len-1 为中序右子树 ——> 放到vin_right
// root_vin+1 ~ len-1 为前序右子树 ——> 放到pre_right
// 3、分别对左右子树递归,返回根节点、
int len = vin.size();
if(len == 0) return NULL;
// 1、建立根节点
TreeNode* head = new TreeNode(pre[0]);
// 找到root_vin
int root_vin = 0;
for(int i=0; i<len; i++){
if(vin[i] == pre[0]){
root_vin = i;
break;
}
}
// 2、
vector<int> pre_left, pre_right, vin_left, vin_right;
for(int i=0; i<root_vin; i++){
vin_left.push_back(vin[i]);
pre_left.push_back(pre[i+1]);
}
for(int i=root_vin+1; i<len; i++){
vin_right.push_back(vin[i]);
pre_right.push_back(pre[i]);
}
// 3、
head->left = reConstructBinaryTree(pre_left, vin_left);
head->right = reConstructBinaryTree(pre_right, vin_right);
return head;
}
};
8、二叉树的下一个节点
1、直接找,分为两种情况:
- 有右子树:下一节点就是右节点中的最左子树
- 无右子树:其下一节点一定是父节点路径上的第一个右父节点
class Solution {
public:
TreeLinkNode* GetNext(TreeLinkNode* pNode)
{
// 1、有右子树:其下一节点就是右节点中的最左子树
// 2、没有右子树:其下一个节点一定是父节点路径上第一个右父节点
if(pNode == NULL) return NULL;
// 1
if(pNode->right != NULL){
pNode = pNode->right; // 右节点
while(pNode->left != NULL){ // 右节点中的最左子树
pNode = pNode->left;
}
return pNode;
}
// 2
while(pNode->next != NULL){
if(pNode->next->left == pNode) // 第一个右父节点
return pNode->next;
pNode = pNode->next;
}
return NULL;
}
};
2、先中序遍历,再从中序遍历的结果中找。
栈和队列
9、用两个队列实现栈
栈:先进后出
队列:先进先出
1、入队:直接push到stack1;出队:先将栈1压入到栈2,然后从栈2再pop出来。
class Solution
{
public:
// 队列:先进先出
// 栈:先进后出
void push(int node) {
stack1.push(node);
}
int pop() {
// 先将栈1压入栈2
if(stack2.empty()){
while(!stack1.empty()){
int tmp;
tmp = stack1.top();
stack2.push(tmp);
stack1.pop();
}
}
if(stack2.empty())
printf("queue is empty");
// 出栈
int result;
result = stack2.top();
stack2.pop();
return result;
}
private:
stack<int> stack1;
stack<int> stack2;
};
算法和数据结构:
- 通常排序和查找是面试重点。
- 重点掌握二分查找、归并排序和快速排序。
- 若要求在二维数组(迷宫或棋盘)上搜索路径 —— 回溯法。
- 若要求某个问题的最优解,且该问题可以分为多个子问题时 —— 动态规划。
- 在动态规划基础上,若分解子问题是不是存在某个特殊的选择,采用这个特殊选择将一定得到最优解 —— 贪婪算法。
- 位运算可以看成一类特殊用法:与、或、异或、左移与右移。
递归和循环
- 递归:
- 在一个函数的内部调用这个函数自身。
- 若面试官没要求,可以尽量使用递归,毕竟代码简单。
- 递归由于是函数调用自身,而函数调用是有时间和空间消耗的:每一次调用函数,都需要在内存栈分配空间以保存参数、返回地址及临时变量,而在往栈里压入数据和弹出数据都需要时间。
- 递归的本质是把一个问题分解成两个或多个小问题,若多个小问题存在相互重叠部分,就存在重复计算。
- 递归可能引起调用栈溢出:当递归调用层级太多时,就会超出栈的容量,从而导致栈溢出。
- 循环:
- 设置计算的初值及终止条件,在一个范围内重复运算。
10、斐波那契数列
1、如果直接用递归,复杂度太高,为O( 2 n 2^n 2n)。
2、使用循环:从下往上计算,先计算f(0)+f(1),在计算f(1)+f(2)。。。以此类推。
class Solution {
public:
int Fibonacci(int n) {
// 递归容易出现内存溢出,且时间复杂度高,故使用循环
// 先计算0,1
int result[2] = {0,1};
if(n<2) return result[n];
// 再计算2及以上
int F_one = 1; // f(n-1)
int F_two = 0; // f(n-2)
int F_n = 0;
for(unsigned int i=2; i<=n; i++){
F_n = (F_one + F_two);
F_two = F_one;
F_one = F_n;
}
return F_n;
}
};
本题扩展:
1、青蛙跳台阶:一只青蛙一次可以跳1级台阶,也可以跳2级台阶。求该青蛙跳上一个n级台阶总共有多少种跳法。
- 只有一级 -> 一种
- 只有两级 -> 两种(一次跳一级;一次跳两级)
- 有n>2级时 -> 第一次跳的时候就有两种不同选择:一是一次只跳一级,此时跳法数目等于后面剩下的n-1级台阶的跳法数目,即为f(n-1);而是第一次跳2级,此时跳法数目等于后面剩下的n-2级台阶的跳法数目,即f(n-2)。故,n级台阶的不同跳法总数是f(n) = f(n-1) + f(n-2)。
2、变态青蛙跳:一只青蛙一次可以跳1级台阶,也可以跳2级台阶…, 它也可以跳上n级。求该青蛙跳上一个n级台阶总共有多少种跳法。
- 利用数学归纳法:
- f ( n ) = f ( n − 1 ) + f ( n − 2 ) + . . . + f ( 1 ) f(n)=f(n-1) + f(n-2) + ... + f(1) f(n)=f(n−1)+f(n−2)+...+f(1)
- f ( n − 1 ) = f ( n − 2 ) + f ( n − 3 ) + . . + f ( 1 ) f(n-1)= f(n-2) + f(n-3) + .. + f(1) f(n−1)=f(n−2)+f(n−3)+..+f(1)
- 两式相减得 f ( n ) = 2 f ( n − 1 ) f(n)=2f(n-1) f(n)=2f(n−1),总数为 f ( n ) = 2 ( n − 1 ) f ( 1 ) f(n) = 2^{(n-1)}f(1) f(n)=2(n−1)f(1),其中 f ( 1 ) = 1 f(1)=1 f(1)=1
3、矩形覆盖:同样是斐波那契数列的变形
- f(n) = f(n-1) + f(n-2)
查找和排序
-
查找:
- 顺序查找、二分查找、哈希表查找和二叉排序树查找。
- 哈希表和二叉树查找重点在数据结构,而不是算法。
- 哈希表主要优点是能够在O(1)时间内查找某一元素,但是需要额外的空间。
-
排序:
- 排序比查找复杂一些,会比较插入排序、冒泡排序、归并排序、快速排序等不同算法的优劣(额外空间消耗,时间复杂度和最差时间复杂度)。
- 面试时最好问清排序应用的环境、哪些约束条件,然后选择合适的排序算法。
11、旋转数组的最小数字
1、二分查找变形:注意考虑边界条件和当数组中数字相同的情况(使用顺序查找)
class Solution {
public:
int minNumberInRotateArray(vector<int> rotateArray) {
// 1、边界条件
// 2、双指针法(二分查找),P1 P2分别指向前后两个顺序序列
// 3、主循环:
// 二分查找 P_mid = (P1 + P2) / 2 进行范围缩小
// 要判断当有P1==P2 && P2==P_mid情况采用直接顺序查找
// 1
if(rotateArray.size() == 0) cout << "rotateArray is empty" << endl;
// 2
int len = rotateArray.size();
int P1 = 0;
int P2 = len - 1;
int P_mid = P1; // 返回P_mid处的值
// 3 注意此处是 P1>= P2,即前面序列大于后面序列时一直循环
while(rotateArray[P1] >= rotateArray[P2]){
if(P2 - P1 == 1){
P_mid = P2;
break;
}
P_mid = (P1 + P2) / 2;
if(rotateArray[P1] == rotateArray[P2] && rotateArray[P2] == rotateArray[P_mid])
return InOrder(rotateArray, P1, P2);
if(rotateArray[P1] <= rotateArray[P_mid])
P1 = P_mid;
else if(rotateArray[P2] >= rotateArray[P_mid])
P2 = P_mid;
}
return rotateArray[P_mid];
}
int InOrder(vector<int> rotateArray, int P1, int P2){
int result;
for(int i = P1; i < P2-1; i++){
if(rotateArray[i] > rotateArray[i+1]){
result = rotateArray[i+1];
break;
}
}
return result;
}
};
上面这个在牛客网可以通过,但是在leetcode就不行,习面可以在leetcode通过
class Solution {
public:
int minArray(vector<int>& numbers) {
int len = numbers.size();
if(len == 0) return 0;
// 双指针,直到找到第一个 num[p1] > num[p2]时
int p1 = 0;
int p2 = len - 1;
while(p1 < p2){
int mid = (p1 + p2) / 2;
if(numbers[mid] > numbers[p2]) p1 = mid + 1;
else if(numbers[mid] < numbers[p2]) p2 = mid;
else p2--;
}
return numbers[p1];
}
};
回溯法
- 暴力法的升级版。
- 非常适合由多个步骤组成的问题,并且每个步骤有多个选项。
- 解决的问题的所有选项可以形象地用树状结构表示。
12、矩阵中的路径
1、回溯法(递归):主体思路就是遍历矩阵找一个起点,然后递归找到字符串所有的路径。
- 先主体遍历矩阵,找一个路径起点
- 再在起点的基础上找附近一格的字符,如果有匹配就下一个,否则返回上一个点附近再找。
- 注意使用布尔值矩阵来进行走过路径的标识,使用memset函数,程序结束完成记得delete[]。
- 注意[row * cols + col]表示的是(row, col)位置。
- 直到路径字符串上的所有字符都在矩阵中找到合适的位置(此时,str[pathLength] == ‘\0’)。
class Solution {
public:
bool hasPath(char* matrix, int rows, int cols, char* str)
{
// 回溯法:
// 1、循环遍历每个格子作为起点
// 2、递归直到路径上的所有字符都在矩阵中找到想应的位置,并且使用布尔值矩阵标识路径是否已经进入格子
// 3、所有字符都在矩阵中找到合适的位置(此时str[pathLength] == '\0')
// 边界条件
if(matrix == NULL || rows <= 0 || cols <= 0; str == NULL)
return false;
// 布尔矩阵
bool *visit = new bool[rows * cols];
memset(visit, 0, rows * cols); // 被填充的矩阵,填充数字,填充长度
int pathLength = 0;
for(int row = 0; row < rows; row++){
for(int col = 0; col < cols; col++){
if(hasPathCore(matrix, rows, cols, row, col, str, visit, pathLength))
return true;
}
}
// delete: 释放new分配的【单个对象】指针指向的内存
// delete[]: 释放new分配的【对象数组】指针指向的内存
delete[] visit;
return false;
}
bool hasPathCore(const char* matrix, int rows, int cols, int row, int col,
const char* str, bool* visit, int pathLength){
if(str[pathLength] == '\0') // 最后一个是’\0’则完成
return true;
bool haspath = false;
if(rows>=0 && row<rows
&& cols>=0 && col < cols
&& matrix[row*cols + col] == str[pathLength]
&& !visit[row*cols + col])
{
++pathLength;
visit[row*cols + col] = true;
haspath = hasPathCore(matrix, rows, cols, row - 1, col, str, visit, pathLength)
|| hasPathCore(matrix, rows, cols, row + 1, col, str, visit, pathLength)
|| hasPathCore(matrix, rows, cols, row, col - 1, str, visit, pathLength)
|| hasPathCore(matrix, rows, cols, row, col + 1, str, visit, pathLength);
if(!haspath){
--pathLength;
visit[row * cols + col] = false;
}
}
return haspath;
}
};
13、机器人的运动范围
1、也是使用回溯法
class Solution {
public:
int movingCount(int threshold, int rows, int cols)
{
// 和矩阵中的路径类似
if(threshold < 0 || rows <=0 || cols <= 0)
return 0;
// 布尔值标识矩阵
bool *visited = new bool[rows * cols];
for(int i=0; i<rows*cols; i++)
visited[i] = false;
int count = movingCountCore(threshold, rows, cols, 0, 0, visited);
delete[] visited;
return count;
}
// 核心算法,递归得到count
int movingCountCore(int threshold, int rows, int cols,
int row, int col, bool* visited){
int count = 0;
if( check(threshold, rows, cols, row, col, visited) )
{
visited[row * cols + col] = true;
// 累加, 其实如果在(0,0) 周围都不满足阈值,也可以得到count
count = 1 + movingCountCore(threshold, rows, cols, row + 1, col, visited)
+ movingCountCore(threshold, rows, cols, row - 1, col, visited)
+ movingCountCore(threshold, rows, cols, row, col + 1, visited)
+ movingCountCore(threshold, rows, cols, row, col - 1, visited);
}
return count;
}
// 检测是否满足阈值条件,数位之和 <= 阈值
bool check(int threshold, int rows, int cols, int row, int col, bool* visited)
{
if( row >= 0 && row < rows && col >= 0 && col < cols // 注意row 和 col 的范围
&& getDigitSum(row) + getDigitSum(col) <= threshold
&& !visited[row * cols + col] )
return true;
return false;
}
// 计算得到数位之和(就是每一位数之和,例如35为 3+5 = 8)
int getDigitSum(int number)
{
int sum = 0;
// 循环得到每个位数
while(number > 0){
sum += number % 10;
number /= 10;
}
return sum;
}
};
动态规划与贪婪算法
- 动态规划的四个特点:
- 求一个问题的最优解
- 整体问题的最优解依赖各个子问题的最优解
- 小问题之间有相互重叠的更小子问题
- 从上往下分析问题,从下往上求解问题
- 贪婪算法和动态规划不一样,当应用贪婪算法解决问题时,每一步都可以做出一个贪婪的选择,基于这个选择,能够得到最优解。
14、剪绳子
1、动态规划算法:O(n2)\O(n)。
- 循环遍历找到最大乘积,f(n)=max(f(i) * f(n-i))
- 从下而上的顺序计算,先得到f(2)、f(3),再得到f(4)、f(5),直到f(n)。
class Solution {
public:
int cutRope(int number) {
// 动态规划:自下往上累加
// 先确定固定的number小的结果
// 两个for循环来确定最大值,分解成小问题的最优解
// 1
if(number<2) return 0; // 1*0
if(number == 2) return 1; // 1*1
if(number == 3) return 2; // 1*2
// 2
int *products = new int[number+1];
products[0] = 0;
products[1] = 1;
products[2] = 2;
products[3] = 3;
int max = 0;
for(int i=4; i<=number; i++){
max = 0;
for(int j=1; j<=i/2; j++){
int product = products[j] * products[i-j]; // j + i - j = i
if(max < product)
max = product;
products[i] = max;
}
}
max = products[number];
delete[] products;
return max;
}
};
2、贪婪算法
- 当n>=5 时,尽可能多的剪长度为3的绳子;
- 当剩下的长度为4时,把绳子剪成长度为2的绳子。
- 数学解释可以参考:当固定总长度n时,长和宽在什么情况下,达到面积最大?结论是:截出的子长度相等时,面积最大。
class Solution {
public:
int cutRope(int number) {
// 贪婪算法:固定策略
// n>=5 尽可能多剪3,n=4,剪2
// 1
if(number<2) return 0; // 1*0
if(number == 2) return 1; // 1*1
if(number == 3) return 2; // 1*2
// 2 尽可能多的剪3
int timeOf3 = number/3;
// 此时更好的方法是剪成长度为2的两
if(number - timeOf3*3 == 1)
timeOf3 -= 1;
int timeOf2 = (number - timeOf3 * 3) / 2;
return int(pow(3, timeOf3) * pow(2, timeOf2));
}
};
位运算
- 五种位运算:与、或、异或、左移和右移
- 左移运算符 m<<n 表示把m左移n位。在左移n位的时候,最左边的n位将被丢弃,同时在最右边补上n个0。比如:
- 00001010 << 2 = 00101000
- 10001010 << 3 = 01010000
- 右移运算符 m>>n 表示把 m 右移n 位。在右移n位的时候,最右边n位将被丢弃。但处理最左边位的情形要复杂一些。
- 如果数字是一个无符号数值,则用0填补最左边的n位;
- 如果数字是一个有符号数值,则用数字的符号位填补最左边的n位。
- 【也就是说,如果数字原先是一个正数,则右移之后在最左边补n个0;如果数字原先是负数,则右移之后在最左边补n个1】
14、二进制中的个数
- 把整数减去1,再和原整数做与运算,会把该整数最右边的1变成0。那么一个整数的二进制表示中有多少个1,就可以进行多少次这样的操作。
class Solution {
public:
int NumberOf1(int n) {
int count = 0;
while(n)
{
++count;
n = (n-1) & n;
}
return count;
}
};
后续的练习
18、删除链表中重复的节点
-
删除一个链表的两种方法:
- 1、从头遍历到要删除的节点i,把i之前的节点->next指向i的下一个节点j,再删除节点i。
- 2、把节点j的内容复制覆盖节点i,接下来再把节点i的->next指向j的下一个节点,再删除节点j。
-
要全面考虑重复节点所处的位置,以及删除重复节点之后的后果
class Solution {
public:
ListNode* deleteDuplication(ListNode* pHead)
{
if(pHead==NULL || pHead->next==NULL)
return pHead;
ListNode* Head = new ListNode(0); // 新建头节点
Head->next = pHead;
ListNode* pre = Head; // 辅助节点
ListNode* cur = Head->next; //当前节点
while(cur != NULL){
if(cur->next != NULL && cur->val == cur->next->val){
// 找到最后一个相同的节点
while(cur->next != NULL && cur->val == cur->next->val){
cur = cur->next;
}
pre->next = cur->next; // 直接接入当前节点的下一节点
cur = cur->next;
}
else{
pre = pre->next; // 继续往前走
cur = cur->next;
}
}
return Head->next;
}
};
19、正则表达式匹配
- 考察思维的全面性,考虑好普通字符 ‘.’ ‘*’,并分析它们的匹配模式。测试代码时要充分考虑排列组合的尽可能多的方式
一、首先,考虑特殊情况:
1>两个字符串都为空,返回true
2>当第一个字符串不空,而第二个字符串空了,返回false(因为这样,就无法
匹配成功了,而如果第一个字符串空了,第二个字符串非空,还是可能匹配成
功的,比如第二个字符串是“a*a*a*a*”,由于‘*’之前的元素可以出现0次,
所以有可能匹配成功)
二、之后就开始匹配第一个字符,这里有两种可能:匹配成功或匹配失败。但考虑到pattern
下一个字符可能是‘*’, 这里我们分两种情况讨论:pattern下一个字符为‘*’或
不为‘*’:
1>pattern下一个字符不为‘*’:这种情况比较简单,直接匹配当前字符。如果
匹配成功,继续匹配下一个;如果匹配失败,直接返回false。注意这里的
“匹配成功”,除了两个字符相同的情况外,还有一种情况,就是pattern的
当前字符为‘.’,同时str的当前字符不为‘\0’。
2>pattern下一个字符为‘*’时,稍微复杂一些,因为‘*’可以代表0个或多个。
这里把这些情况都考虑到:
a>当‘*’匹配0个字符时,str当前字符不变,pattern当前字符后移两位,
跳过这个‘*’符号;
b>当‘*’匹配1个或多个时,str当前字符移向下一个,pattern当前字符
不变。(这里匹配1个或多个可以看成一种情况,因为:当匹配一个时,
由于str移到了下一个字符,而pattern字符不变,就回到了上边的情况a;
当匹配多于一个字符时,相当于从str的下一个字符继续开始匹配)
class Solution {
public:
bool match(char* str, char* pattern)
{
if(str == NULL && pattern==NULL)
return false;
return matchCore(str, pattern);
}
bool matchCore(char* str, char* pattern)
{
// 1、字符和模式都为空'\0', 返回true
if(*str == '\0' && *pattern == '\0')
return true;
// 2、字符不为空,模式为空,肯定匹配不上,返回false
if(*str != '\0' && *pattern == '\0')
return false;
// 3、如果模式的下一个(第二个)字符位'*'
if(*(pattern+1) == '*')
{
// 如果字符串和模式第一个字符相匹配
if(*str == *pattern || (*pattern == '.' && *str != '\0')){
return matchCore(str+1, pattern) // 字符串后移一个字符,模式不变
|| matchCore(str+1, pattern+2) // 字符串后移一个字符,模式后移两个,'*'和其前面的字符可以算空
|| matchCore(str, pattern+2); // 字符串不动,模式后移两个,'*'和其前面的字符可以算空
}
else
return matchCore(str, pattern+2);
}
// 4、pattern下一个不为'*',直接匹配,如果匹配成功,返回字符,否则返回false。
if(*str == *pattern || (*pattern == '.' && *str != '\0'))
{
return matchCore(str+1, pattern+1);
}
return false;
}
};
20、表示数值的字符串
- 遵循模式A[.[B]][e|EC]或者.B[e|EC]
class Solution {
public:
bool isNumeric(char* string)
{
// A[.[B]][e|EC]
if(string == NULL)
return false;
bool numeric = scanInteger(&string);
// '.'后面可以是数字的小数部分,点前面可以没有整数;点后面可以没有数字;点前后都可以有数字
if(*string == '.')
{
++string;
numeric = scanUnsignedInteger(&string) || numeric;
}
// 'e|E' 后面是指数部分,前面必须有数字,可以是小数;后面必须有整数
if(*string == 'e' || *string == 'E')
{
++string;
numeric = scanInteger(&string) && numeric;
}
return numeric && *string == '\0';
}
// 匹配B
bool scanUnsignedInteger(char** string)
{
char* before = *string;
while(**string != '\0' && **string >= '0' && **string <= '9')
++(*string);
return *string > before;
}
// 匹配AC
bool scanInteger(char** string)
{
if(**string == '+' || **string == '-')
++(*string);
return scanUnsignedInteger(string);
}
};
21、调整数组顺序使奇数位于偶数前面
- 借鉴冒泡排序的方法,让偶数不断向右边移动,冒泡过程本身不改变顺序。时间复杂度O(n)
class Solution {
public:
void reOrderArray(vector<int> &array) {
int len = array.size();
if(len == 0 )
return;
// 冒泡排序
while(len--){
bool change = false;
for(int i=0; i<len; i++)
{
// 判断前后两个数据奇偶并交换
if(array[i] % 2 == 0 && array[i+1] % 2 == 1){
Swap(array, i, i+1);
change = true;
}
}
if(!change)
return;
}
}
void Swap(vector<int> &array, int i, int j){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
};
22、链表中倒数第k个节点
- 注意边界条件,节点数是否大于k
- 双指针,先让快指针走k步,然后让慢指针指向头结点,然后开始同时走,快指针指向尾节点,则慢节点就是倒数第k个节点。
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
// 边界条件
if(pListHead == NULL || k == 0)
return NULL;
ListNode* pAhead = pListHead;
ListNode* pBehid = NULL;
for(unsigned int i=0; i<k-1; i++){
if(pAhead->next != NULL)
pAhead = pAhead->next;
else
return NULL;
}
pBehid = pListHead;
while(pAhead->next != NULL){
pAhead = pAhead->next;
pBehid = pBehid->next;
}
return pBehid;
}
};
23、链表中环的入口节点
- 快慢指针,快指针遇到环形指针后,慢指针会追上快指针。
- 第二次新建个指针,让这个指针和慢指针同时走,慢指针遇到环之后必定和新建的指针在环形入口相遇,这时候随意返回一个指针。
- 参考
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
if(pHead == NULL)
return NULL;
ListNode* fast = pHead;
ListNode* slow = pHead;
while(fast != NULL && fast->next != NULL){
fast = fast->next->next;
slow = slow->next;
if(fast == slow){
ListNode* slow2 = pHead;
// 让slow2去和slow相遇,相遇的节点即为入口
while(slow2 != slow){
slow = slow->next;
slow2 = slow2->next;
}
return slow2;
}
}
return NULL;
}
};
24、反转链表
- 1、使用递归(目前没特别理解)
- 使用递归函数,一直递归到链表的最后一个结点,该结点就是反转后的头结点,记作 ret
- 此后,每次函数在返回的过程中,让当前结点的下一个结点的 next 指针指向当前节点
- 同时让当前结点的 next 指针指向 NULL ,从而实现从链表尾部开始的局部反转
- 当递归函数全部出栈后,链表反转完成。
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
if(pHead == NULL || pHead->next == NULL)
return pHead;
ListNode *ret = ReverseList(pHead->next); // 递归
pHead->next->next = pHead;
pHead->next = NULL;
return ret;
}
};
- 2、双指针
- 双指针局部反转,然后同时前移指针,知道尾节点
- 注意边界条件
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
if(pHead == NULL && pHead->next == NULL)
return pHead;
ListNode *cur = pHead; // cur在前
ListNode *last = NULL;
while(cur != NULL){
ListNode *temp = cur->next;
cur->next = last; // 局部翻转
last = cur; // 同时前移
cur = temp;
}
return last;
}
};
25、合并两个排序的链表
- 1、递归的方法
- 注意空指针的排查
- 比较两个链表的头节点的值,大的放在后面
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
if(pHead1 == NULL)
return pHead2;
if(pHead2 == NULL)
return pHead1;
ListNode *pMergeNode = NULL;
// 链表1的值小于链表2的值
if(pHead1->val < pHead2->val){
pMergeNode = pHead1;
pMergeNode->next = Merge(pHead1->next, pHead2); // 将2放在1后
}
else{
pMergeNode = pHead2;
pMergeNode->next = Merge(pHead1, pHead2->next); // 否则1放在2后
}
return pMergeNode;
}
};
- 2、迭代求解
- 定义cur指向新链表的头结点
- 如果p1指向的节点值小于等于p2指向的节点值,则将p1指向的节点值连接到cur的next指针,然后p1指向下一个节点值
- 否则,让p2指向下一个节点值
- 循环上述两步,知道p1或者p2为NULL
- 将p1和p2剩下的部分链接到cur后面
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
ListNode *vHead = new ListNode(-1); // 哨兵节点
ListNode *cur = vHead;
while(pHead1 && pHead2){
if(pHead1->val <= pHead2->val)
{
cur->next = pHead1;
pHead1 = pHead1->next;
}
else
{
cur->next = pHead2;
pHead2 = pHead2->next;
}
cur = cur->next;
}
cur->next = pHead1 ? pHead1:pHead2;
return vHead->next;
}
};
26、树的子结构
- 第一步:在树A中找到和树B的根节点的值一样的节点R;
- 第二步:判断树A中以R为根节点的子树是不是包含和树B一样的结构。
- 【注意:判断节点值相等时,不能直接写 == ,因为计算及在判断double时都有误差,如果误差很小,则认为相等】
class Solution {
public:
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2)
{
bool result = false;
// 递归
if(pRoot1 != NULL && pRoot2 != NULL)
{
if(Equal(pRoot1->val, pRoot2->val)) // 注意此处判断是double 相等, 不能直接用等号来判断
result = DoesTreeHaveTree2(pRoot1, pRoot2);// 进行第二步判断
if(!result)
result = HasSubtree(pRoot1->left, pRoot2);
if(!result)
result = HasSubtree(pRoot1->right, pRoot2);
}
return result;
}
// 判断第二步:如果根节点相同之后,就判断左右节点是否相同
bool DoesTreeHaveTree2(TreeNode* pRoot1, TreeNode* pRoot2)
{
if(pRoot2 == NULL)
return true;
if(pRoot1 == NULL)
return false;
if(!Equal(pRoot1->val, pRoot2->val))
return false;
return DoesTreeHaveTree2(pRoot1->left, pRoot2->left)
&& DoesTreeHaveTree2(pRoot1->right, pRoot2->right);
}
bool Equal(double num1, double num2)
{
if((num1 - num2 > -0.0000001) && (num1 - num2 < 0.0000001))
return true;
else
return false;
}
};
【精简版的代码】
class Solution {
public:
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2)
{
return (pRoot1 != NULL && pRoot2 != NULL)
&& (recur(pRoot1, pRoot2)
|| HasSubtree(pRoot1->left, pRoot2)
|| HasSubtree(pRoot1->right, pRoot2));
}
bool recur(TreeNode* A, TreeNode* B){
if(B == NULL) return true;
if(A == NULL || A->val != B->val) return false;
return recur(A->left, B->left) && recur(A->right, B->right);
}
};
小结:面试时一般在白板上编写,要注意
- 规范性:书写清晰、布局清晰、命名合理
- 完整性:完成基本功能、考虑边界条件、做好错误处理
- 鲁邦性:采取防御性变成、处理无效的输入
27、二叉树的镜像
- 先前序遍历这棵树的每个节点,如果遍历到的节点有子节点,就交换它的两个子节点;
- 当交换所有非叶节点的左右子节点之后,便得到了树的镜像。
class Solution {
public:
void Mirror(TreeNode *pRoot) {
if(pRoot == NULL) // 注意边界条件
return;
// 非递归
queue<TreeNode*> pq;
pq.push(pRoot);
while(!pq.empty()){
int pq_size = pq.size();
while(pq_size--){
TreeNode* node = pq.front();
pq.pop();
// 层序遍历,将左右节点依次压入栈,供下次读取
if(node->left) pq.push(node->left);
if(node->right) pq.push(node->right);
// 交换节点
TreeNode* temp = node->left;
node->left = node->right;
node->right = temp;
}
}
}
};
28、对称的二叉树
- 通过比较二叉树的前序遍历序列和对称前序遍历序列来判断二叉树不是对称的。如果两个序列是一样的,则二叉树就是对称的。
class Solution {
public:
bool isSymmetrical(TreeNode* pRoot)
{
return isSymetricalCore(pRoot, pRoot);
}
bool isSymetricalCore(TreeNode* pRoot1, TreeNode* pRoot2){
if(pRoot1 == NULL && pRoot2 == NULL)
return true;
if(pRoot1 == NULL || pRoot2 == NULL)
return false;
if(pRoot1->val != pRoot2->val)
return false;
return isSymetricalCore(pRoot1->left, pRoot2->right) // 前序遍历
&& isSymetricalCore(pRoot1->right, pRoot2->left); // 对称前序遍历
}
};
29、顺时针打印矩阵
- 第一步必须需要,如果只有一行,则只有第一步;
- 如果终止行号大于起始行号,则需要第二步,也就是竖着打印;
- 如果至少两行两列,也就是除了要求终止行号大于起始行号,还要终止列号大于起始列号;
- 同理,打印第四步的条件是至少有三行两列,因此要求终止行号比起始行号至少大2,同时终止列号大于起始列号
class Solution {
public:
vector<int> printMatrix(vector<vector<int> > matrix) {
int rows = matrix.size();
int cols = matrix[0].size();
vector<int> result;
int start = 0;
if(cols <= 0 && rows <= 0)
return result;
while(rows > start*2 && cols > start*2){
int endX = cols - 1 - start; // 终止列
int endY = rows - 1 - start; // 终止行
for(int i=start; i<=endX; ++i){
result.push_back(matrix[start][i]);
}
if(start < endY){
for(int i=start+1; i<=endY; ++i){
result.push_back(matrix[i][endX]);
}
}
if(start<endX && start<endY){
for(int i=endX-1; i>=start; --i){
result.push_back(matrix[endY][i]);
}
}
if(start<endX && start<endY-1){
for(int i=endY-1; i>=start+1; --i){
result.push_back(matrix[i][start]);
}
}
++start;
}
return result;
}
};
通过举实例让问题抽象化
30、包含min函数的栈
- 使用辅助栈的思路,每次都把最小元素压入辅助栈,保证辅助栈的栈顶一直都是最小元素。
class Solution {
public:
stack<int> Src;
stack<int> Min;
void push(int value) {
Src.push(value);
if(Min.empty() || value<=Min.top()){
Min.push(value);
}
}
void pop() {
if(Src.top() == Min.top())
Min.pop();
Src.pop();
}
int top() {
return Src.top();
}
int min() {
return Min.top();
}
};
31、栈的压入、弹出序列
- 如果下一个弹出的数字刚好是栈顶数字,则直接弹出;如过下一个弹出的数字不在栈顶,则把压栈序列中还没有入栈的数字压入辅助栈,直到把下一个需要弹出的数字压入栈顶为止;如果所有数字都压入栈后仍然没有找到下一个弹出的数字,则该序列不可能是一个弹出序列。
class Solution {
public:
bool IsPopOrder(vector<int> pushV,vector<int> popV) {
// 新建一个栈,将数组A压入栈中,当栈顶元素等于数组B时,该元素出栈,当循环结束时判断,该栈是否为空,空则返回true
if(pushV.size() == 0 || popV.size() == 0 || pushV.size() != popV.size())
return false;
stack<int> stack;
int j = 0;
for(int i=0; i<pushV.size(); i++){
stack.push(pushV[i]);
while(!stack.empty() && stack.top() == popV[j]){
stack.pop();
j++;
}
}
return stack.empty(); // 为空则返回true,否则位false
}
};
32、从上往下打印二叉树
- 考察树的遍历算法,层序遍历吧
- 每次打印一个节点的时候,如果该节点有子节点,则把该节点的子节点放到一个队列的末尾,接下来队列的头部取出最早进入队列的节点,重复前面操作,直到所有节点被打印出来。
- 本题使用标准库deque队列
class Solution {
public:
vector<int> PrintFromTopToBottom(TreeNode* root) {
vector<int> result;
if(!root)
return result;
std::deque<TreeNode*> dequeRoot; // 使用标准库的 deque 队列,两端都可以进出
dequeRoot.push_back(root);
while(dequeRoot.size())
{
TreeNode *pNode = dequeRoot.front();
dequeRoot.pop_front();
result.push_back(pNode->val);
if(pNode->left)
dequeRoot.push_back(pNode->left);
if(pNode->right)
dequeRoot.push_back(pNode->right);
}
return result;
}
};
33、二叉搜索树的后续遍历
-
二叉搜索树:又称二叉排序树,二叉查找树,
- 若它的左子树不为空,则左子树上所有节点的值均小于它的根节点的值;
- 若它的右子树不为空,则右子树上所有节点的值均大于它的根节点的值;
-
后续遍历:
- 先遍历左子节点,再右子节点,再根节点
-
本题:
- 先判断根节点左子树是否有大于根节点的值,再判断右子树;最后再通过递归来判断左右子树的子树。
class Solution {
public:
bool VerifySquenceOfBST(vector<int> sequence) {
// 先判断根节点左子树是否有大于根节点的值,再判断右子树
// 最后再递归判断左右子树是否符合
int length = sequence.size();
if(length <= 0)
return false;
int root = sequence[length - 1];
// 判断左子树
int i = 0;
vector<int> sequence_left;
for(; i<length-1; i++){
if(sequence[i] > root)
break;
sequence_left.push_back(sequence[i]);
}
// 判断右子树
int j = i;
vector<int> sequence_right;
for(; j<length-1; j++){
if(sequence[j] < root)
return false;
sequence_right.push_back(sequence[j]);
}
// 递归判断左右子树
bool left = true;
if(i > 0)
left = VerifySquenceOfBST(sequence_left);
bool right = true;
if(j < length-1)
right = VerifySquenceOfBST(sequence_right);
return(left && right);
}
};
34、二叉树中和为某一值的路径
分解让复杂问题简单化
- 分治法,即分而治之。通常分治法可以用递归代码实现
35、复杂链表的复制
- 分解问题:1、根据原始链表的每个节点N创建对应N’;2、设置复制出来的节点random;3、把长链表拆成复制链表
class Solution {
public:
RandomListNode* Clone(RandomListNode* pHead)
{
// 分解问题:1、根据原始链表的每个节点N创建对应N';2、设置复制出来的节点random;3、把长链表拆成复制链表
if(pHead == NULL) return NULL;
CloneNode(pHead);
ConnectRandomNode(pHead);
return ReconnectNode(pHead);
}
void CloneNode(RandomListNode* pHead)
{
RandomListNode* pNode = pHead;
while(pNode != NULL){
RandomListNode* pCloned = new RandomListNode(pNode->label); // 注意此处的new
pCloned->next = pNode->next;
pCloned->label = pNode->label;
pCloned->random = NULL;
pNode->next = pCloned;
pNode = pCloned->next;
}
}
void ConnectRandomNode(RandomListNode* pHead)
{
RandomListNode* pNode = pHead;
while(pNode != NULL){
RandomListNode* pCloneNode = pNode->next;
if(pNode->random != NULL){
pCloneNode->random = pNode->random->next;
}
pNode = pCloneNode->next;
}
}
RandomListNode* ReconnectNode(RandomListNode* pHead){
RandomListNode* pNode = pHead;
RandomListNode* pCloneHead = NULL;
RandomListNode* pClodeNode = NULL;
if(pNode != NULL){
pCloneHead = pClodeNode = pNode->next;
pNode->next = pClodeNode->next;
pNode = pNode->next;
}
while(pNode != NULL){
pClodeNode->next = pNode->next;
pClodeNode = pClodeNode->next;
pNode->next = pClodeNode->next;
pNode = pNode->next;
}
return pCloneHead;
}
};
36、二叉树搜索与双向链表(理解不透彻)
- 1、由于二叉搜索树中序遍历是按照顺序排好的,当遍历到根节点的时候,把树分成三部分,根节点左子树,根节点,根节点右子树
- 2、先把根节点与左右子树双向链表链接,再递归左右子树
class Solution {
public:
TreeNode* Convert(TreeNode* pRootOfTree)
{
// 1、由于二叉搜索树中序遍历是按照顺序排好的,当遍历到根节点的时候,把树分成三部分,根节点左子树,根节点,根节点右子树
// 2、先把根节点与左右子树双向链表链接,再递归左右子树
TreeNode* pLastNodeList = NULL;
ConvertNode(pRootOfTree, &pLastNodeList);
TreeNode *pHeadOfList = pLastNodeList;
while(pHeadOfList != NULL && pHeadOfList->left != NULL)
pHeadOfList = pHeadOfList->left;
return pHeadOfList;
}
void ConvertNode(TreeNode* pNode, TreeNode** pLastNodeList){
if(pNode == NULL) return;
TreeNode* pCurrent = pNode;
if(pCurrent->left != NULL)
ConvertNode(pCurrent->left, pLastNodeList);
pCurrent->left = *pLastNodeList;
if(*pLastNodeList != NULL)
(*pLastNodeList)->right = pCurrent;
*pLastNodeList = pCurrent;
if(pCurrent->right != NULL)
ConvertNode(pCurrent->right, pLastNodeList);
}
};
37、序列化二叉树(理解不透彻)
- 可以根据前序遍历的顺序来序列化二叉树,因为前序遍历是从根节点开始的,在遍历二叉树碰到NULL指针时,这些NULL指针序列化为一个特殊的字符,如‘$’。另外,节点的数值之间用’,'隔开
class Solution {
private:
TreeNode* Decode(char *&str){ // &str是内存地址,*&str表示&str指向地址内存空间的值
if(*str == '$'){
str++;
return NULL;
}
int num = 0;
while(*str != ',')
num = num*10 + (*(str++) - '0'); //
str++;
TreeNode *root = new TreeNode(num);
root->left = Decode(str);
root->right = Decode(str);
return root;
}
public:
char* Serialize(TreeNode *root) {
//
if(!root) return "$";
string s = to_string(root->val);
s.push_back(',');
char* left = Serialize(root->left);
char* right = Serialize(root->right);
char* result = new char[strlen(left) + strlen(right) + s.size()];// 新建字符串保存序列
strcpy(result, s.c_str()); // 复制
strcat(result, left); // 合并
strcat(result, right);
return result;
}
TreeNode* Deserialize(char *str) {
return Decode(str);
}
};
38、字符串的排列
- 递归法:先固定第一个字符,求剩余字符的排列
- 1、遍历出所有可能出现第一个位置的字符
- 2、固定第一个字符,递归后面的字符
class Solution {
public:
vector<string> Permutation(string str) {
// 递归法,先固定第一个字符,求剩余字符的排列
// 1、遍历出所有可能出现第一个位置的字符
// 2、固定第一个字符,求后面字符的排列
vector<string> result;
if(str.empty()) return result;
PermutationCore(str, result, 0);
sort(result.begin(), result.end());
return result;
}
void PermutationCore(string str, vector<string> &result, int begin){ // 传入result地址
if(begin == str.size() - 1){// 递归结束条件,索引已近指向str最后一个元素时
if(find(result.begin(), result.end(), str) == result.end())
result.push_back(str); // 如果result中不存在str,才添加。避免重复添加的情况
}
else{
// 第一次循环i与begin相等,相当于第一个位置自身交换,关键在于之后的循环
// 之后i != begin,则会交换两个不同位置上的字符,直到begin==str.size() - 1,进行输出
for(int i=begin; i<str.size(); i++){
swap(str[i], str[begin]);
PermutationCore(str, result, begin+1);
swap(str[i], str[begin]); // 复位,用以恢复之前字符串顺序,达到第一位依次跟其他位交换的目的
}
}
}
void swap(char &first, char &second){
char temp = first;
first = second;
second = temp;
}
};
优化时间空间效率
- 养成采用引用或指针传递复杂类型参数的习惯
- 由于递归可能会产生相互重叠的部分,效率可能会很差。可以采用
- 递归的思路分析问题,但写代码的时候可以用数组来保存中间结果基于
39、数组中出现次数超过一半的数字
- 思路1:在遍历数组时保存两个值:一个是次数,一个是元素。
遍历下一个数字是,若它与之前保存的数字相同,则次数加1,否则次数减1
若次数位0,则保存下一个数字,并将次数置1
遍历结束后,所保存的数字即为所求 - 时间复杂度 O(n)
class Solution {
public:
int MoreThanHalfNum_Solution(vector<int> numbers) {
if(numbers.empty()) return 0;
// 遍历每个元素,并记录次数,若与前一个元素相同,则次数加1,否则次数减1
int result = numbers[0];
int cnt = 1; // 次数
for(int i=0; i<numbers.size(); ++i){
if(cnt == 0){
result = numbers[i];
cnt = 1;
}
else if(numbers[i] == result)
++cnt; // 相同则加1
else
--cnt; // 不同则减1
}
// 判断result是否符合条件,即出现次数大于数组长度的一半
cnt = 0;
for(int i=0; i<numbers.size(); ++i){
if(numbers[i] == result) ++cnt;
}
return (cnt > numbers.size()/2) ? result : 0;
}
};
- 思路2:先排序,再遍历,中位数上的数字即为结果。时间复杂度O(nlogn)
class Solution {
public:
int MoreThanHalfNum_Solution(vector<int> numbers) {
if(numbers.empty()) return 0;
sort(numbers.begin(), numbers.end());
int middle = numbers[numbers.size() / 2];
int count = 0;
for(int i=0; i<numbers.size(); ++i){
if(numbers[i] == middle) ++count;
}
return (count > (numbers.size()/2)) ? middle : 0;
}
};