【剑指offer-第二版】习题感悟(一刷)
- 1. 面试流程
- 2. 面试的基础知识
- 3. 高质量的代码
- 面试题16:数值的整数次方
- 面试题17:删除链表的结点
- 面试题18:调整数组顺序使奇数位于偶数前面
- 面试题19:链表中倒数第k个节点
- 面试题23:链表中环的入口节点
- 面试题24:反转链表
- 面试题25:合并两个排序的链表
- 面试题25.1:合并k个已排序的链表
- 面试题26:树的子结构
- 面试题27:二叉树的镜像
- 面试题28:对称的二叉树
- 面试题29:顺时针打印矩阵
- 面试题30:包含min函数的栈
- 面试题31:栈的压入、弹出序列
- 面试题32:从上到下打印二叉树
- 面试题33:二叉搜索树的后序遍历序列
- 面试题34:二叉树中和为某一值的路径
- 面试题35:复杂链表的复制
- 面试题36:二叉搜索树与双向链表
- 面试题37:在排序数组中查找数字
- 面试题38: 0~n-1中缺失的数字
- 面试题39:二叉搜索树的第k大节点
- 面试题40:二叉树的深度
- 面试题41:判断平衡二叉树
- 面试题42:数组中数字出现的次数
- 面试题43:数组中唯一出现一次的数字
- 面试题44:和为s的两个数
- 面试题45:和为s的连续正整数序列
- 面试题46:翻转单词顺序
- 面试题47:左旋转字符串
- 面试题48:滑动窗口的最大值
- 面试题49:队列中的最大值
- 面试题60:n个骰子的点数
- 面试题61:扑克牌中的顺子
- 面试题62:股票的最大利润
- 面试题63:求解1+2+...+n
- 面试题64:不用加减乘除做加法
- 面试题65:树中两个节点的最低公共祖先
- 面试题66:二叉树的公共祖先
- 面试题67:删掉一个元素以后全为 1 的最长子数组
- 面试题68:分割回文串(阿里实习笔试)
1. 面试流程
一般分为三轮面试:2-3轮技术面,1轮HR面
每一轮技术面试分为3个环节:行为面试,技术面试,提问环节
- 行为面试:自我介绍,按照简历上内容进行项目的提问;
- 技术面试:编程语言,数据结构与算法等基础知识,手撕代码的能力等;
- 提问环节:推荐提问和应聘职位相关的问题,或者项目团队氛围怎么样等等。
2. 面试的基础知识
2.1 C++基础知识
面试题1:赋值运算符函数
class CMyString
{
private:
char* m_pData;
public:
CMyString(const char* pData = nullptr);
CMyString(const CMyString& str);
~CMyString(void);
CMyString& operator=(const CMyString& str)
{
if(this != &str)
{
// 调用拷贝构造函数,作用域之外直接析构释放内存
CMyString strTemp(str);
// 交换临时变量
char* pTemp = strTemp.m_pData;
strTemp.m_pData = m_pData;
m_pData = pTemp;
}
return *this;
}
};
CMyString::CMyString(const char* pData)
{
if(pData == nullptr)
{
m_pData = new char[1];
m_pData ='\0';
}
else
{
m_pData = new char[strlen(pData)+1];
strcpy_s(m_pData, strlen(pData)+1, pData);
}
}
面试题2:实现单例模式
单例模式:设计一个类,只能生成该类的一个实例。
#include <iostream>
#include <mutex>
using namespace std;
class CSingleton{
private:
CSingleton(){}
static CSingleton* m_pInstance;
static mutex mux_;
public:
static CSingleton *GetInstance()
{
if(m_pInstance == nullptr)
{
lock_guard<mutex> lock(mux_);
m_pInatance = new CSingleton();
}
return m_pInstance;
}
};
CSingleton* CSingleton::m_pInstance = nullptr;
2.2 数据结构
面试题3:数组中重复的数字
题目1:找出数组中重复的数字
// 解法1:使用hash表
bool duplicate(int number[], int length, int* duplication)
{
if(number == nullptr || length <=0)
return false;
int * hashTable = new int[length]();
for(int i=0; i<length; i++)
{
if(hashTable[number[i]])
{
*duplication = number[i];
return true;
}
else{
hashTable[number[i]] = 1;
}
}
delete[] hashTable;
return false;
}
题目2:不修改数组找出重复的数字
// 哈希表求解
int duplicate(int number[], int length)
{
if(number == nullptr || length<=0)
return -1;
int hashTable = new int[length]();
for(int i=0; i<length; i++)
{
if(hashTable[number[i]])
return i;
else
hashTable[number[i]] = 1;
}
delete[] hashTable;
return -1;
}
面试题4:二维数组中查找
// 思路:按照从右上角查找比较,按照比较结果删除一列或者一行
bool findMatrix(int* matrix, int rows, int cols, int number)
{
if(matrix != nullptr && cols>0 && rows>0)
{
int row = 0;
int col = cols-1;
while(row<rows && col>=0)
{
if(matrix[row*cols+col] == number)
return true;
else if(matrix[row*cols+col] > number)
col--;
else
row++;
}
}
return false;
}
面试题5:替换字符串中的空格
// 思路:统计字符串的长度和空格个数,然后从后向前检索替换,注意临界条件
void ReplaceBlank(char string[], int length)
{
if (string == nullptr || length <= 0)
return;
int origenLength = 0;
int blankLength = 0;
int i = 0;
while (string[i] != '\0')
{
origenLength++;
if (string[i] == ' ')
blankLength++;
++i;
}
int newLength = origenLength + blankLength * 2;
if (newLength > length)
return;
while (newLength > origenLength || origenLength >= 0)
{
if (string[origenLength] == ' ')
{
string[newLength--] = '0';
string[newLength--] = '2';
string[newLength--] = '%';
}
else
string[newLength--] = string[origenLength];
origenLength--;
}
}
// 使用string
string ReplaceBlank(string s)
{
string ss;
for(int i=0; i<s.size(); i++)
{
s[i]==' ' ? ss+="%20" : ss+=s[i];
}
return ss;
}
面试题6:从尾到头打印链表
// 思路:使用栈存储,实现先进后出
void ListReverse(ListNode* head)
{
stack<ListNode*> stk;
ListNode* node = head;
while(node != nullptr)
{
stk.push(node);
node = node->next;
}
while(!stk.empty())
{
cout<<stk.top()->value<<" ";
stk.pop();
}
}
面试题7:重建二叉树
// 根据前序遍历,中序遍历重建二叉树
// 思路:先根据前序遍历找到根结点,在利用根结点信息在中序遍历中查找根结点序号。最后递归嗲用构建左右子树
class TreeNode
{
public:
int m_value;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : m_value(x), left(nullptr), right(nullptr) { }
};
TreeNode* BuildTree1(vector<int> preorder, vector<int> inorder)
{
if (preorder.size() != inorder.size() || preorder.size() == 0)
return nullptr;
int root = preorder[0];
TreeNode* node = new TreeNode(root);
int root_idx = -1;
for (int i = 0; i < inorder.size(); ++i)
{
if (inorder[i] == root)
{
root_idx = i;
break;
}
}
if (root_idx == -1)
return nullptr;
int leftsize = root_idx;
int rightsize = inorder.size() - root_idx - 1;
node->left = BuildTree(vector<int>(preorder.begin() + 1, preorder.begin() + leftsize + 1), vector<int>(inorder.begin(), inorder.begin() + leftsize));
node->right = BuildTree(vector<int>(preorder.end() - rightsize, preorder.end()), vector<int>(inorder.end() - rightsize, inorder.end()));
return node;
}
面试题8:二叉树的下一个节点
// 思路:可以分为两种情况
// 情况1:该节点存在右子树,则下一节点就是右子树的最左子节点
// 情况2:该节点不存在右子树,可根据该节点的父节点的左子结点是否与本身相同判断,然后确定下一个子节点
class TreeLinkNode
{
int m_value;
TreeLinkNode* left;
TreeLinkNode* right;
TreeLinkNode* parent;
TreeLinkNode(int x) : m_value(x), left(nullptr), right(nullptr), parent(nullptr){}
};
TreeLinkNode* GetNextNode(TreeLinkNode* pNode)
{
if(pNode == nullptr) return nullptr;
TreeLinkNode* pNext = nullptr;
if(pNode->righe != nullptr)
{
TreeLinkNode* pRight = pNode->right;
whiel(pRight->left != nullptr)
pRight = pRight->left;
pNext = pRight;
}
else
{
TreeLinkNode* pParent = pNode;
while(pParent->parent != nullptr)
{
if(pParent->parent->left == pParent)
{
pNext = pParent->parent;
break;
}
pParent = pParent->parent;
}
}
return pNext;
}
面试题9:两个栈实现队列or两个队列实现栈
// 思路:利用一个栈专门用于出队列,另一个栈用于入队列
class cQueue
{
public:
cQueue() {}
~cQueue() {}
// 实现入队列
void appendTail(const int& node)
{
stack1.push(node);
}
// 实现出队列
int deleteHead()
{
//if(stack.empty() && stack2.empty())
//return ;
if(stack2.empty())
{
while(!stack1.empty())
{
stack2.push(stack1.top());
stack1.pop();
}
}
int n = stack2.top();
stack2.pop();
return n;
}
private:
stack<int> stack1;
stack<int> stack2;
};
// 两个队列实现栈
// 思路:保证其中一个栈为空
class cStack
{
public:
cStack() {}
~cStack() {}
void appendHead(const int& node)
{
if (!que1.empty())
{
que1.push(node);
}
else
{
que2.push(node);
}
}
int deteleNode()
{
//if (que1.empty() && que2.empty())
//return ;
int node = 0;
if (!que1.empty())
{
int num = que1.size();
while (num > 1)
{
que2.push(que1.front());
que1.pop();
--num;
}
node = que1.front();
que1.pop();
}
else
{
int num = que2.size();
while (num > 1)
{
que1.push(que2.front());
que2.pop();
--num;
}
node = que2.front();
que2.pop();
}
return node;
}
private:
queue<int> que1;
queue<int> que2;
};
2.3 算法和数据操作
面试题10:斐波那契数列
// 思路:采用递归的方式调用效率低,原因在于存在重复的调用
// 变种,小青蛙跳台阶(根据跳的方式累加)
int Fn(int n)
{
if(n<=1) return n;
int result = 0; // 保存结果 f(n)
int pre = 0; // f(n-2)
int cur = 1; // f(n-1)
for(int i=2; i<=n; i++)
{
// 计算f(n-1)+f(n-2)的结果
result = pre + cur;
pre = cur; // 向下传递
cur = result; // 向下传递
}
return result;
}
快速排序算法
//思路:首先找到系列中的基准(一般选第一个);然后找到起始和终止结点;之后从左到右和从右到左遍历。最后递归实现左右边的排序
void QuickSort(int* data, int start, int end)
{
if(start < end)
{
int std = data[start];
int low = start;
int high = end;
while(low<high)
{
while(low<high && data[high]>std)
--high;
data[low] = data[high];
while(low<high && data[low]<=std)
++low;
data[high] = data[low];
}
data[low] = std;
QuickSort(data, start, low-1);
QuickSort(data, low+1, end);
}
else
return ;
}
面试题11:旋转数组的最小数字
//思路:需要根据序列确定不同情况
// 1.如果序列中包含相同的数字,则需要找该序列的最小值
// 2. 如果序列式正常递增,则需要利用二分法查找
//查找最小数字
int minInorder(vector<int> vec)
{
int temp = vec[0];
for(auto& v:vec)
{
if(v<temp)
temp = v;
}
return temp;
}
int MinOrder(vector<int> rotateVec)
{
int size = rotateVec.size();
if(size == 0) return 0;
if(size == 1 && rotareVec[0]<rotateVec[size-1]) return rotateVec[0];
int low = 0;
int high = size-1;
while((high-low)>1)
{
int mid = (high+low)>>1;
if(rotateVec[mid]==rotateVec[low] && rotateVec[mid]==rotateVec[high])
return minInorder(rotateVec);
if(rotateVec[mid] <= rotateVec[high])
high = mid;
else if(rotateVec[mid] >= rotateVec[low])
low = mid;
}
return rotateVec[high];
}
面试题12:矩阵中的路径
// 思路:使用回溯的方法实现
// 1. 主题
bool hasPath(string matrix, int cols, int rows, string str)
{
if(matrix.size() == 0 || cols<0 || row<0 || str.size() == 0)
return false;
bool* visited = new bool[rows*cols];
memset(visited, 0, rows*cols);
int pathLength = 0;
for(int row=0; row<rows; row++)
{
for(int col=0; col<cols; col++)
{
// 调用回溯算法
if(hasCore(matrix, cols, rows, col, row, str, pathLength, visited))
{
delete[] visited;
return true;
}
}
}
delete[] visited;
return false;
}
// 2. 回溯法核心
bool hasCore(string matrix, int cols, int rows, int col, int row, string str, int& pathLength, bool* visited)
{
if(str[pathLength] == '\0')
return true;
bool haspath = false;
if(row>0 && col>0 && col<cols && row<rows && str[pathLength] == matrix[col+cols*row] && !visited[col+cols*row])
{
pathLength--;
visited[col+cols*row] = true;
haspath = hasCore(matrix, cols, rows, col-1, row, str, pathLength, visited)
|| hasCore(matrix, cols, rows, col+1, row, str, pathLength, visited)
|| hasCore(matrix, cols, rows, col, row-1, str, pathLength, visited)
|| hasCore(matrix, cols, rows, col, row+1, str, pathLength, visited);
if(!haspath)
{
pathLength--;
visited[col+cols*row] = false;
}
}
return haspath;
}
面试题13:机器人的运动范围
// 思路:保证机器人满足一下三个条件,表示可以运动。依旧使用回溯法
// 1. 机器人的下一个位置是边界处
// 2. 机器人已经走过该路径
// 3. 数位之和大于阈值
int getNumber(int data)
{
int sum = 0;
while(data!=0)
{
sum += data%10;
data = data /10;
}
return sum;
}
int CountRobotCore(int threshold, int cols, int rows, int col, int row, bool* visited)
{
if(threshold<(getNumber(col)+getNumber(row)) || col<0 || row<0 ||col>=cols || row>=rows || visited[col+cols*row])
return 0;
int res = 1;
visited[col+cols*row] = true;
res + = CountRobotCore(threshold, cols, rows, col-1, row, visited)
+ CountRobotCore(threshold, cols, rows, col+1, row, visited)
+ CountRobotCore(threshold, cols, rows, col, row-1, visited)
+ CountRobotCore(threshold, cols, rows, col, row+1, visited);
return res;
}
int CountRobot(int threshold, int rows, int cols)
{
if(rows<0 || cols<0 || threshold<0)
return 0;
bool* visited = new bool[cols*rows];
memset(visited,0,rows*cols);
int result = CountRobotCore(threshold, cols, rows, 0, 0, visited);
delete[] visited;
return res;
}
面试题14:剪绳子
// 思路:采用动态规划(DP)或者贪心算法
// 1. 动态规划
int maxDPCut(int length)
{
// 由于最少必须减一次
if(length<2) return 0;
if(length == 2) return 1;
if(length == 3) return 3;
// 当绳子长度大于等于4时,不减绳子的收益大于剪绳子,因此需要重新给小于4的赋值
vector<int> product{0,1,2,3};
int max = 0;
for(int i=4; i<=length; i++)
{
max = 0;
for(int j=1; j<=i/2; j++) // 分为两份,防止重复计算
{
int products = product[j]*product[i-j];
if(max<products)
max = products;
}
product.push_back(max);
}
return product[length];
}
面试题15:二进制中1的个数
总结:
- 位运算包含5种运算:与、或、异或、左移、右移;
- 左移:左边丢弃,右边补0;右移:右边丢弃,左边根据是否具有符号确定补0或1;
- 尽量使用移位操作替代2的整数此房的乘除法,使用位与运算判断一个数的奇偶性(n&1);
- 把一个整数减一后再和原来整数做位与运算,得到的结果相当于把原来整数的二进制表示中最右边的1变成0;
//思路:可以采用自身减一与自身与操作来消除1,此操作便可以用来记录1的个数
int NumberOf1(int n)
{
int count = 0;
while(n)
{
count++;
n = n & (n-1);
}
return count;
}
// 输入两个整数m,n,计算需要改变m的二进制表示中的多少位才能得到n.
int getNumberof1(int m, int n)
{
int count = 0;
int data = m^n; // 使用异或将两者转换为不同位为1, 接下来统计1的个数
while(data)
{
count++;
data = data & (data-1);
}
return count;
}
3. 高质量的代码
面试题16:数值的整数次方
// 思路:需要考虑底数为0,指数为负数的情况
double Power(double base, int exponent)
{
if(base == 0.0 && exponent<0) // 底数为0且指数为负数,直接返回0.0;
return 0.0;
if(exponent== 0)
return 1.0;
int absExp = exponent <0 ? -exponent : exponent;
double result = 1.0;
for(int i=0; i<absExp; i++)
{
result *= base;
}
if(exponent <0)
result = 1.0/result ;
return exponent <0 ? 1.0/result : result;
}
// 递归算法
double pow(double base, int exps)
{
if(exps == 0) return 1.0;
double res = pow(base, exps>>1);
if(exps&1 == 1)
return res * res * base;
else
return res * res;
}
double Power(double base, int exponent)
{
if(base == 0.0) return 0.0;
int AbsExp = exponent < 0 ? -exponent : exponent ;
return exponent < 0 ? 1.0/pow(base, AbsExp) : pow(base, AbsExp);
}
面试题17:删除链表的结点
// 思路:判断头结点是否是要删除的结点,如果是,则返回下一个节点;否则判断下一个节点是要要删除,以此类推
class ListNode
{
public:
int val;
ListNode* next;
ListNode(int v):val(v),next(nullptr) { }
};
ListNode* deleteNode(ListNode* head, int val)
{
if(head->val == val) return head->next;
ListNode* node = head;
whiel(node->next != nullptr)
{
if(node->next->val == val)
{
node->next = node->next->next;
return head;
}
node = node->next;
}
return head;
}
// 思路: 删除链表中重复的数字.
// 首先创建链表,该链表的下一个节点指向头指针,并创建pre与cur用于遍历和删除节点
ListNode* deleteDupli(ListNode* head)
{
if(head==nullptr) return head;
ListNode* dyNode = new ListNode(-1);
dyNode->next = head;
ListNode* pre = dyNode;
ListNode* cur = head;
while(cur)
{
if(cur->next && cur->next->val == cur->val) // 如果遇到重复节点
{
while(cur->next && cur->next->val == cur->val) // 遍历跳过
cur= cur->next;
pre->next = cur->next; // 删除重复的结点
}
else // 如果没遇到重复结点
{
pre = cur; // 移动前驱结点
}
cur = cur->next; // 不管有没有重复节点,都移动cur
}
return dyNode->next; //返回链表的下一个节点(不包括头结点)
}
面试题18:调整数组顺序使奇数位于偶数前面
// 思路:使用双指针遍历,左边指针如果是奇数,则叠加;右边指针是偶数,则叠加
vector<int> recorder(vector<int>& vec)
{
int left = 0;
int right = vec.size()-1;
while(left<right)
{
if((vec[left]&1) == 1)
{
left++;
continue;
}
if((vec[right]&1) == 0)
{
right--;
continue;
}
swap(vec[left++], vec[right--]);
}
return vec;
}
面试题19:链表中倒数第k个节点
// 思路:使用两个指针,保证快指针移动k个节点,慢指针在快指针移动k个节点后开始移动,直到快指针为空指针
ListNode* FindKnode(ListNode* head, int k)
{
if(head == nullptr || k<=0)
{
return nullptr;
}
ListNode* pre = head;
ListNode* cur = head;
for(int i=0; i<k; i++)
{
if(cur!=nullptr)
{
cur = cur->next;
}
else
{
return nullptr; // 说明k大于链表长度
}
}
while(cur!=nullptr)
{
cur = cur->next;
pre = pre->next;
}
return pre;
}
// 快慢指针遍历
ListNode* midNode(ListNode* head)
{
if(head == nullptr) return nullptr;
ListNode* fast = head;
ListNode* slow= head;
while(fast && fast->next)
{
fast = fast->next->next;
slow= slow->next;
}
return sold;
}
面试题23:链表中环的入口节点
// 思路:快指针路径是慢指针路径的2倍,使用快慢指针找到相遇节点,然后从头开始遍历链表,直到和slow相遇,返回慢指针节点
ListNode* EntryLoop(ListNode* head)
{
if(head == nullptr) return nullptr;
ListNode* fast=head;
ListNode* slow=head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(fast==slow)
{
ListNode* node = head;
while(node != slow) // 两者路径长度相同 (a+b+c+b) = 2(a+b) => a = c
{
node = node->next;
slow = slow->next;
}
return slow; // 找到环入口即返回
}
}
return nullptr; // 表示没有环
}
面试题24:反转链表
// 思路:使用三个指针进行保存,分别为当前指针、前一个指针、后一个指针
ListNode* reverseList(ListNode* head)
{
ListNode* cur = head;
ListNode* pre = nullptr;
ListNode* next = nullptr;
while(cur != nullptr)
{
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
return pre;
}
面试题25:合并两个排序的链表
// 思路:采用非递归的方式,按照大小顺序进行递增
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2)
{
ListNode* node = new ListNode(-1);
ListNode* retNode = node;
while(l1 && l2)
{
if(l1->val > l2->val)
{
node->next = l2;
l2 = l2->next;
}
else
{
node->next = l1;
l1 = l1->next;
}
node = node->next;
}
if(l2 == nullptr) node->next = l1;
if(l1 == nullptr) node->next = l2;
return retNode->next;
}
面试题25.1:合并k个已排序的链表
// 思路:属于上一个的变种。只需要遍历即可
ListNode* mergeKLists(vector<ListNode*> &lists)
{
int len = lists.size();
if(len == 0) return nullptr;
ListNode* head = lists[0];
for(int i=1; i<len; i++)
{
}
}
ListNode* twoLists(ListNode* l1, ListNode* l2)
{
ListNode* node = new ListNode(-1);
ListNode* n = node;
while(l1 && l2)
{
if(l1->val > l2->val)
{
node->next = l2;
l2 = l2->next;
}
else{
node->next = l1;
l1 = l1->next;
}
node = node->next;
}
node->next = (l1==nullptr)? l2 : l1;
return n->next;
}
面试题26:树的子结构
// 思路:使用递归的方式,两种递归,1是递归遍历寻找与子树根结点相同;2是找到与子树根结点相同后,开始遍历比较两个子树,递归遍历
class TreeNode
{
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int v) : val(v), left(nullptr), right(nullptr) { }
};
// 主函数,用于递归查找与根结点是否相同
bool SubTree(TreeNode* A, TreeNode* B)
{
if(A==nullptr || B==nullptr) return false;
bool SubFlag = false;
if(A->val == B->val) SubFlag = AhaveB(A, B);
if(!SubFlag) SubFlag = SubTree(A->left, B);
if(!SubFlag) SubFlag = SubTree(A->right, B);
return SubFlag;
}
// 子函数:用于递归比较与子树是否相同
bool AhaveB(TreeNode* A, TreeNode* B)
{
if(B==nullptr) return true;
if(A==nullptr) return false;
if(A->val != B->val) return false;
return AhaveB( A->left, B->left) && AhaveB( A->right, B->right);
}
面试题27:二叉树的镜像
// 思路:递归遍历,先交换左右子树的结点,然后进行左右子树递归
TreeNode* mirroeTree(TreeNode* root)
{
if(root == nullptr) return nullptr; // 递归结束条件
TreeNode* temp = root->left; // 交换左右子树
root->left = root->right;
root->right = temp;
mirrorTree(root->left); // 递归左子树
mirrorTree(root->right); // 递归左子树
return root;
}
面试题28:对称的二叉树
// 思路:判断二叉树是否为对称的,即左子树的左 == 右子树的右, 左子树的右 == 右子树的左
// 主函数
bool isSymmetric(TreeNode* root)
{
if(root == nullptr) return true;
return Mirror(root->left, root->right);
}
// 子函数
bool Mirror(TreeNode* left, TreeNode* right)
{
// 先判断结点为空的情况
if(left==nullptr && right==nullptr) return true;
if(left==nullptr || right==nullptr) return false;
// 判断结点不为空时,结点值不同
if(left->val != right->val) return false;
bool outSide = Mirror(left->left, right->right);
bool inSide = Mirror(left->right, right->left);
return outSide && inSide;
}
面试题29:顺时针打印矩阵
// 思路:首先判断矩阵是否为空,然后在开始打印,按照:从左到右,从上到下,从右到左,从下到上的顺序
vector<int> spiralOrder(vector<vector<int>>& matrix) {
if(matrix.size()==0 || matrix[0].size()==0) return {};
int rows = matrix.size();
int cols = matrix[0].size();
vector<int> result;
int row = 0, col = -1;
while(rows>0 && cols>0)
{
// 从左到右
for(int i=0; i<cols; i++)
{
col++;
result.push_back(matrix[row][col]);
}
rows--;
if(rows==0 || cols==0) break; // 一旦存在行或列为空,则需要跳出来
// 从上到下
for(int i=0; i<rows; i++)
{
row++;
result.push_back(matrix[row][col]);
}
cols--;
if(rows==0 || cols==0) break;
// 从右到左
for(int i=0; i<cols; i++)
{
col--;
result.push_back(matrix[row][col]);
}
rows--;
if(rows==0 || cols==0) break;
// 从下到上
for(int i=0; i<rows; i++)
{
row--;
result.push_back(matrix[row][col]);
}
cols--;
if(rows==0 || cols==0) break;
}
return result;
}
面试题30:包含min函数的栈
// 思路:添加一个辅助栈,用来保存每添加一个数据时的最小值
class MinStack {
public:
/** initialize your data structure here. */
stack<int> m_data, m_min;
MinStack() { }
void push(int x) {
m_data.push(x);
if(m_min.empty())
{
m_min.push(x);
}
else
{
int temp = m_min.top();
if(temp > x)
m_min.push(x);
else
m_min.push(temp);
}
}
void pop() {
m_data.pop();
m_min.pop();
}
int top() {
return m_data.top();
}
int min() {
return m_min.top();
}
};
面试题31:栈的压入、弹出序列
// 思路:添加一个辅助占, 每次想辅助栈中添加元素,都需要用栈顶数据跟弹出序列的数据进行比较,如果相等,栈就需要出栈顶;同时弹出序列向后移动。压入和弹出序列相等的条件是,辅助栈为空。
bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
bool result = false;
if(pushed.size() != popped.size()) return result;
stack<int> m_data;
int idx = 0;
for(auto& vec : pushed)
{
m_data.push(vec);
while(!m_data.empty() && m_data.top() == popped[idx])
{
m_data.pop();
idx++;
}
}
if(m_data.empty()) result = true;
return result;
}
面试题32:从上到下打印二叉树
// 思路, 使用队列想入先出的方式,不断加入节点
vector<int> levelOrder(TreeNode* root)
{
if(root == nullptr) return {};
queue<TreeNode*> que;
que.push(root);
vector<int> result;
while(!que.empty())
{
TreeNode* node = que.front();
que.pop();
result.push_back(node->val);
if(node->left != nullptr) que.push(node->left);
if(node->right != nullptr) que.push(node->right);
}
return result;
}
// 思路:打印二叉树,每层打印到一行
vector<vector<int>> levelOrder(TreeNode* root)
{
if(root == nullptr) return {};
queue<TreeNode*> que;
que.push(root);
vector<vector<int>> result;
while(!que.empty())
{
vector<int> temp;
int sz = que.size();
while(sz--)
{
TreeNode* node = que.front();
temp.push_back(node->val);
que.pop();
if(node->left != nullptr) que.push(node->left);
if(node->right != nullptr) que.push(node->right);
}
result.push_back(temp);
}
return result;
}
// 思路:之字型打印二叉树; 需要注意偶数层需要反转,奇数层不变
vector<vector<int>> levelOrder(TreeNode* root) {
if(root == nullptr) return {};
queue<TreeNode*> que;
que.push(root);
int level = 0; // 用于记录层数
vector<vector<int>> result;
while(!que.empty())
{
int sz = que.size();
vector<int> temp;
level++;
while(sz--)
{
TreeNode* node = que.front();
temp.push_back(node->val);
que.pop();
if(node->left != nullptr) que.push(node->left);
if(node->right != nullptr) que.push(node->right);
}
if((level & 1) == 0) reserve(temp); // 偶数需要反转
result.push_back(temp);
}
return result;
}
void reserve(vector<int>& vec)
{
int sz = vec.size();
for(int i=0; i<sz/2; i++)
{
int temp = vec[i];
vec[i] = vec[sz-i-1];
vec[sz-i-1] = temp;
}
}
面试题33:二叉搜索树的后序遍历序列
// 思路:判断二叉搜索树(二叉排序树)是否是后序遍历(左右根)
// 主函数
bool versfyPostorder(vector<int>& postorder)
{
if(postorder.size() == 0) return true;
return dfsOrder(postorder, 0, postorder.size()-1);
}
bool dfsOrder(vector<int> vec, int left, int right)
{
if(left >= right) return true;
int root = vec[right];
int i = left;
for(; i<right; i++) // 找到比根结点大的第一个结点位置
{
if(vec[i] > root)
break;
}
for(int j=i; j<right; j++) // 如果右子树存在比根结点小的数,则说明该序列不是后续遍历序列
{
if(vec[j] < root)
return false;
}
return defOrder(vec, left, i-1) && defOrder(vec, i, right-1); // 递归遍历左右子树
}
面试题34:二叉树中和为某一值的路径
// 思路:使用前序递归的形式遍历二叉树,保存路径,然后比较路径之和。最后将所有路径上的数字保存
vector<vector<int>> FindPath(TreeNode* root, int sum)
{
if(root == nullptr) return {};
vector<int> path;
vector<vector<int>> result;
findPath(root, sum, path, result);
return result;
}
void findPath(TreeNode* root, int sum, vector<int>& path, vector<vector<int>>& res)
{
// 1. 对根结点保存
path.push_back(root->val);
// 2. 判断当前是否满足和要求
if(root->val == sum && root->left==nullptr && root->right==nullptr)
{
res.push_back(path);
}
// 3. 不满足和条件,就开始遍历左子树和右子树(需要注意,此时sum改变,需要减掉当前节点的值)
if(root->left) findPath(root->left, sum-root->val, path, res);
if(root->right) findPath(root->left, sum-root->val, path, res);
path.pop_back();
}
面试题35:复杂链表的复制
// 思路:可以使用哈希表进行拷贝复制
class Node
{
public:
int val;
Node* next;
Node* random;
Node(int _val) {val = _val; next = NULL; random = NULL;}
};
Node* copyRandomList(Node* head)
{
if(head == nullptr) return nullptr;
// 1. 创建哈希表,并遍历链表
unordered_map<Node*, Node*> u_map;
Node* cur = head;
while(curr != nullptr)
{
u_map[cur] = new Node(cur->val);
cur = cur->next;
}
// 2. map映射复制
cur = head;
while(cur != nullptr)
{
u_map[cur]->next = u_map[cur->next];
u_map[cur]->random = u_map[cur->random];
cur = cur->next;
}
return u_map[head];
}
面试题36:二叉搜索树与双向链表
// 思路:由于是二叉搜索树,因此需要采用中序遍历。
class Solution{
public:
Node* treeToDoublyList(Node* root)
{
if(root == nullptr) return nullptr;
inOrder(root);
head->left = pre;
pre->right = head;
return head;
}
private:
Node* pre;
Node* head;
void inOrder(Node* cur)
{
if(cur == nullptr) return ;
// 中序遍历
inOrder(cur->left);
if(pre != nullptr) pre->right = cur;
else head = cur;
cur->left = pre;
pre = cur;
inOrder(cur->right);
}
};
面试题37:在排序数组中查找数字
// 思路:可用哈希表,或者 二分法查找。 其中哈希表较为占用内存
int search(vector<int>& nums, int target)
{
return BinaryLeft(nums, target+1)-BinaryLeft(nums, target);
}
int BinaryLeft(vector<int>& nums, int target)
{
int left = 0;
int right = nums.size()-1;
int mid = 0;
while(left<=right)
{
mid = (left+right)>>1;
if(nums[mid] < target)
{
left = mid+1;
}
else
{
right = mid-1;
}
}
return left;
}
面试题38: 0~n-1中缺失的数字
// 思路:使用二分法查找,如果相等则在右边,否则在左边
void missingNumber(vector<int>& nums)
{
return BinaryNum(nums);
}
int BinaryNum(vector<int> nums)
{
int left = 0;
int right = nums.size()-1;
int mid = 0;
while(left<=right)
{
mid = (left+right)>>1;
if(nums[mid] == mid)
left = mid + 1;
else
right = mid - 1;
}
return left;
}
面试题39:二叉搜索树的第k大节点
// 思路: 使用中序遍历可以得到二叉搜索树按照从小到大的排序,而我们需要从大到小的排序,因此需要按照从右到左
int kthLargest(TreeNode* root, int k)
{
if(root == nullptr || k==0) return 0;
vector<int> result;
mid_dfs(root, result);
return result[k-1];
}
void mid_dfs(TreeNode* root, vector<int>& vec)
{
if(root == nullptr) return ;
mid_dfs(root->right, vec);
vec.push_back(root->val);
mid_dfs(root->left, vec);
}
面试题40:二叉树的深度
// 思路: 采用层序遍历,借助queue
int maxDepth(TreeNode* root)
{
if(root == nullptr) return 0;
queue<TreeNode*> que;
que.push(root);
int depth=0;
while(!que.empty())
{
++depth;
int size = que.size();
while(size--)
{
TreeNode* node = que.front();
que.pop();
if(node->left) que.push(node->left);
if(node->right) que.push(node->right);
}
}
return depth;
}
面试题41:判断平衡二叉树
// 思路:如果时平衡二叉树,则说明任意节点的左、右子树的高度差不超过1.可以使用后序遍历。
bool res = true;
bool idBalance(TreeNode* root)
{
dfs(root);
return res;
}
int dfs(TreeNode* root)
{
if(root == nullptr) return 0;
int left = dfs(root->left);
int right = dfs(root->right);
if(abs(left-right)>1) res = false;
return max(left, right)+1;
}
面试题42:数组中数字出现的次数
// 思路:可以使用异或实现检测数组中出现不重复的数字,如果一个数组中只有一个不重复的数字,仅需要遍历一遍即可。
vector<int> singleNumbers(vector<int>& nums)
{
int first=0, second=0, mid=1;
for(auto &n : nums) // 遍历一遍数组可以找到两个不重复数字的组合
{
first ^= n;
}
while( (first&mid) == 0) // 找到两个数字第一个不同的位,利用该位将数组分为两份,然后分别查找
mid <<=1;
first = 0;
for(auto& n : nums)
{
if(n&mid) first ^= n;
else second ^= n;
}
return {first, second};
}
面试题43:数组中唯一出现一次的数字
// 思路:可以使用哈希表
int singleNumber(vector<int>& nums)
{
int res = 0;
unordered_map<int, int> map_n;
for(auto &n:nums) map_n[n]++;
for(auto &n:nums)
{
if(map_n[n] == 1)
{
res = n;
break;
}
}
return res;
}
面试题44:和为s的两个数
// 思路:使用双指针,前后各一个指针。循环向前逼近
vector<int> twoSum(vector<int>& nums, int target)
{
int begin = 0;
int end = nums.size()-1;
while(end > begin)
{
int res = nums[begin]+nums[end];
if(res > target) end--;
else if(res < target) begin++;
else return vector<int>{nums[begins], nums[end]};
}
return vector<int>();
}
面试题45:和为s的连续正整数序列
// 思路:可以使用滑动窗口算法,左闭右开。主要判断条件时 左边界小于目标值的一半
vector<vector<int>> findContinuousSequence(int target)
{
int left = 1; // 左边界
int right = 1; // 右边界
int sum = 0; // 中间值
vector<vector<int>> result;
while(left <= target/2)
{
if(sum < target)
{
sum += right;
right++;
}
else if(sum > target)
{
sum -= left;
left++;
}
else{
vector<int> res;
for(int i=left; i<right; i++)
{
res.push_back(i);
}
result.push_back(res);
sum -= left;
left++;
}
}
return result;
}
面试题46:翻转单词顺序
// 思路:采用反转单词顺序,同时需要按照:完全反转句子顺序,去除多余空格(多于2个空格),完全翻转后在对每一个单词反转。
string reverseWords(string s)
{
int begin = 0; // 起始反转位置
int end = s.size()-1; // 终止反转点
reserve(s, begin, end); // 反转整个句子
begin = 0;
end = 0;
while(s[end] == '\0') // 如果没有到字符串最后一位
{
if(s[begin] == ' ')
{
s.erase(begin,1); // 移除多余的空格
}
else if(s[end] == ' ')
{
reserve(s, begin, end-1);
end++;
begin = end;
}
else
{
end++;
}
}
if(end>1 && s[end-1] == ' ') s.erase(end-1,1); // 移除最后一个空格
reserve(s, begin, end-1); // 少翻转一次,需要补上(最后一个单词没有反转)
return s;
}
// 反转单词顺序
void reserve(string &s, int begin, int end)
{
int mid = (begin+end)>>1;
while(begin <= mid)
{
char c = s[begin];
s[begin] = s[end];
s[end] = c;
begin++;
end--;
}
}
面试题47:左旋转字符串
// 思路:1.可以使用反转字符串的思路,即先反转前半部分字符串,然后反转后半部分字符串,最后反转整个字符串。2. 或者直接使用字符串拼接,前后拼接,取其中从n到len的字符串
// 1. 反转字符串思路
string reserveLeftWords(string s, int n)
{
if(s.size() <=1 )return s;
int end = s.size()-1;
reserve(s, 0, n-1); // 反转前半部分
reserve(s, n, end); // 反转后半部分
reserve(s, 0, end); // 反转全部
return s;
}
void reserve(string &s, int begin, int end)
{
int mid = (begin+end)>>1;
while(begin <= mid)
{
char c = s[begin];
s[begin] = s[end];
s[end] = c;
begin++;
end--;
}
}
// 2. 拼接方法
string reserveLeftWords(string s, int n)
{
int len = s.size();
s += s; // 拼接字符串
return s.substr(n, len); // 取出(n-len)字符串
}
面试题48:滑动窗口的最大值
// 思路:可以使用双端队列实现查找,具体可以看下面代码
vector<int> maxSlidingWindow(vector<int>& nums, int k)
{
vector<int> result;
deque<int> que;
for(int i = 0; i<nums.size(); i++)
{
// 查找滑窗内的最大值,放到队头
while( !que.empty() && nums[i] > num[que.back()])
que.pop_back();
// 判断队头的索引是否超出窗口左边界
if( !que.empty() && que.front() < i-k+1)
que.pop_front();
que.push_back(i);
if( i >= k-1) result.push_back(nums[que.front()]);
}
return result;
}
面试题49:队列中的最大值
// 思路:使用单调不增序列,同时使用队列和双向队列
class MyQueue{
public:
queue<int> que;
deque<int> deq;
MyQueue()
{
}
int MaxValue()
{
if(deq.empty()) return -1;
return deq.front();
}
void push_back(int value)
{
que.push(value);
while(!deq.empty() && deq.back() < value)
deq.pop_back();
deq.push_back(value);
}
int pop_front()
{
if(que.empty()) return -1;
int value = que.front();
if(value == deq.front())
deq.pop_front();
que.pop();
return value;
}
};
面试题60:n个骰子的点数
// 思路:主要利用动态规划进行求解,其状态转移方程需要构建,还有初始化边界条件
vector<double> dicesProbability(int n)
{
// 首先定义:二维数组res, 表示第n个骰子和为j出现的次数
vector<vector<int>> res(n+1, vector<int>(6*n+1, 0));
// 其次定义初始化条件
for(int i=1; i<=6; i++)
res[1][i] = 1;
// 之后开始循环,使用上一时刻的状态定义下一时刻
for(int i=2; i<=n; i++)
for(int j=2; j<=6*n; j++)
for(int k=1; k<=6; k++)
{
if(j<=k) break;
res[i][j] += res[i-1][j-k];
}
int allSum = pow(6,n); // 表示一共计算的次数,用于计算概率
vector<double> result; // 保存计算结果
// 下面便是将第n个骰子和为j的次数除以总计算次数得到概率
for(int i=n; i<=6*n; i++) // 由于和的范围是[n,6*n]
{
result.push_back(res[n][i]*1.0/allSum);
}
return result;
}
面试题61:扑克牌中的顺子
// 思路:使用先排序,在统计0的个数,之后则判断相邻间数字的间隔与0的大小
bool isStraight(vector<int>& nums)
{
sort(nums.begin(), nums.end());
int zero_size = 0;
int space_sum = 0;
for(int i=0; i<4; i++)
{
if(nums[i] == 0)
{
zero_size++;
continue;
}
int temp = nums[i+1]-num[i]; // 计算数字间隔
if(temp > 1)
space_sum += (temp-1); // 统计相邻数字间的间隔,正常间隔为1
if(temp == 0)
return false; // 存在重复数字,直接返回
}
return (zero_size >= space_sum);
}
面试题62:股票的最大利润
// 思路:只需要一次遍历即可,记录最大利润
int maxProfit(vector<int>& prices)
{
if(prices.size() < 2) return 0; // 少于两个值,输出为0
int min_value = prices[0]; // 记录前面的数据的最低值(买入值)
int max_price = prices[1] - min_value; // 记录最大利润
for(int i=1; i<prices.size(); i++) // 一次遍历
{
if(prices[i] < min_value)
min_value = prices[i];
int dif = prices[i]-min_value;
if(dif > max_price)
max_price = dif;
}
return max_price;
}
面试题63:求解1+2+…+n
// 思路:由于不能使用乘除,for、 if等运算符和关键字. 可以使用递归或者计算内存的方式
int sumNums(int n)
{
n && (n += sumNums(n-1));
return n;
}
// 利用计算内存 sizef
int sumNums(int n)
{
bool a[n][n+1];
return (sizeof(a)>>1);
}
面试题64:不用加减乘除做加法
// 思路:使用 位运算进行计算, 分别计算 进位和没进位的加法
// 进位 (a&b)<<1;
// 没进位的加法 a^b;
int add(int a, int b)
{
while(b != 0) // 进位不为0
{
int c = (unsigned int)(a&b)<<1;
a = a^b;
b = c;
}
return a;
}
面试题65:树中两个节点的最低公共祖先
// 思路:由于是二叉搜索树, 因此可以同时比较树的值
// 如果当前节点比两个节点值都大,说明公共节点在左子树;如果比两个节点都小,说明公共节点都在右字树。其他情况说明当前节点就是公共节点或者公共节点是两者中的一个
TreeNode* LowerCommon(TreeNode* root, TreeNode* p, TreeNode* q)
{
TreeNode* node = root;
while(true)
{
if(node->val > p->val && node->val > q->val)
node = node->left;
else if(node->val < p->val && node->val < q->val)
node = node->right;
else
break;
}
return node;
}
面试题66:二叉树的公共祖先
// 思路:由于是普通二叉树, 需要遍历查找。采用后序遍历
TreeNode* lowestCommon(TreeNode* root, TreeNode* p, TreeNode* q)
{
if(!root || !p || !q || root==p || root=q) // 表示当前当前节点就是两个节点中一个
return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q); // 后序遍历
if(!left && !right) return nullptr; // 表示节点两边都没有
else if(left && right) return root; // 表示在当前节点两边
else if(!left && right) return right; // 表示在右节点这边
else return left; // 表示在左节点这边
}
面试题67:删掉一个元素以后全为 1 的最长子数组
// 思路:可以使用滑动窗口进行统计,主要统计滑动窗口内0的个数。如果0个数大于1则需要移动左边界,将多余的0移除出去;如果0个数小于等于1则需要移动右边界
int longestSubarray(vector<int>& nums)
{
// 首先定义左右边界
int left=0, right=0;
int size=nums.size(); // 统计数组的大小
int count=0; // 表示窗口内0的个数
int res=0; // 表示最后的滑窗内最大连续1值
while(right<size) // 移动到右边界
{
count += nums[right]==0; // 统计滑窗内0的个数
while(count >1)
{
count -= nums[left]==0; // 将移除掉的0剪掉
left++;
}
res = max(res, right-left+1); // 保存最大窗口数
right++; // 说明滑窗内0个数小于等于1
}
return res-1; // 由于需要减掉0,因此需要为窗口最大值减1
}
// 包含输入输入输出的笔试
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
// 滑动窗口算法
int maxSlideWindow(vector<int> nums)
{
int left = 0, right = 0;
int count = 0;
int size = nums.size();
int res = 0;
while (right < size)
{
count += nums[right] == 0;
while (count > 1)
{
count -= nums[left] == 0;
left++;
}
res = max(res, right - left + 1);
right++;
}
return res - 1;
}
// 主函数
int main()
{
int count = 0;
cin >> count;
vector<vector<int>> nums;
while (count--)
{
int signlCount = 0;
cin >> signlCount;
int temp = 0;
vector<int> nums1;
while (signlCount--)
{
cin >> temp;
nums1.push_back(temp);
}
nums.push_back(nums1);
}
int length = nums.size();
for (auto & num : nums)
{
int res = maxSlideWindow(num);
cout << res << " ";
}
cout << endl;
//system("pause");
return 0;
}
面试题68:分割回文串(阿里实习笔试)
// 思路,使用动态规划
// d[n][k]表示前n个字符被分割为k个回文子串至少需要修改的字符数
// 1. 判断回文子串所需要修改的字符数
int cost(string& s, int l, int r)
{
int res = 0;
for(int i=l, j=r; i<j; i++, j--)
{
if(s[i] != s[j])
res++;
}
return res;
}
// 2. 动态规划返回d[n][k]
int reslotion(string& s, int k)
{
int n=s.size(); // 字符个数
// dp[i][j]表示前i个字符分割为j个子串需要的最少字符数,其中i>=j
vector<vector<int>> dp(n+1, vector<int>(k+1, 1e6));
dp[0][0] = 0; // 状态初值
for(int i=1; i<=n; i++) // 前i个字符串
{
for(int j=1; j<=min(i,k); j++) // 表示分割为j个回文子串, 且j<=i以保证足够分割
{
if(j==1) // 表示前i个不分割,直接统计
{
dp[i][j] = cost(s, 0, i-1);
}
else
{
for(int t=j-1; t<i; t++) // 将j个子串再分为 j-1个子串和1个回文串之和,以此类推
{
dp[i][j] = min(dp[i][j], dp[t][j-1]+cost(s, t, i-1)); // 状态转移方程,使用最小的
}
}
}
}
return dp[n][k];
}
int main()
{
int size = 0;
cin>>size;
vector<string> str;
vector<int> count;
int temp;
while(size--)
{
string temp_str;
cin>>temp_str;
str.push_back(temp_str);
cin>>temp;
count.push_back(temp);
}
for(int i=0; i<str.size(); i++)
{
cout<<reslotion(str[i], count[i])<<endl;
}
return 0;
}