牛客网题解
二维数组的查找
二维数组
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
Consider the following matrix:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
Given target = 5, return true.
Given target = 20, return false.
思路
要求时间复杂度 O(M + N),空间复杂度 O(1)。其中 M 为行数,N 为 列数。
该二维数组中的一个数,小于它的数一定在其左边,大于它的数一定在其下边。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间,当前元素的查找区间为左下角的所有元素。
题解
class Solution {
public:
bool Find(int target, vector<vector<int> > array) {
int R = array.size();
if (R == 0) return false;
int C = array[0].size();
int lo = 0, hi = C-1;
while(lo<R && hi>=0){
if(array[lo][hi] == target) return true;
else if (array[lo][hi] > target){
hi--;
}else{
lo++;
}
}
return false;
}
};
替换空格
字符串
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
Input:
"A B"
Output:
"A%20B"
思路
① 在字符串尾部填充任意字符,使得字符串的长度等于替换之后的长度。因为一个空格要替换成三个字符(%20),所以当遍历到一个空格时,需要在尾部填充两个任意字符。
② 令 P1 指向字符串原来的末尾位置,P2 指向字符串现在的末尾位置。P1 和 P2 从后向前遍历,当 P1 遍历到一个空格时,就需要令 P2 指向的位置依次填充 02%(注意是逆序的),否则就填充上 P1 指向字符的值。从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。
③ 当 P2 遇到 P1 时(P2 <= P1),或者遍历结束(P1 < 0),退出。
题解
class Solution {
public:
void replaceSpace(char *str,int length) {
int P1 = length-1;
int P2 = P1;
for(int i = 0; i<length; i++){
if(*(str+i) == ' ') P2+=2;
}
while(P1>=0 && P1!=P2){
if(*(str+P1) == ' '){
*(str+(P2--)) = '0';
*(str+(P2--)) = '2';
*(str+(P2--)) = '%';
P1--;
}else{
*(str+(P2--)) = *(str+(P1--));
}
}
}
};
- 类似的解法
class Solution {
public:
void replaceSpace(char *str,int length) {
int count=0;
for(int i=0;i<length;i++){
if(str[i]==' ')
count++;
}
for(int i=length-1;i>=0;i--){
if(str[i]!=' '){
str[i+2*count]=str[i];
}
else{
count--;
str[i+2*count]='%';
str[i+2*count+1]='2';
str[i+2*count+2]='0';
}
}
}
};
从尾到头打印链表
链表,栈,递归
从尾到头反过来打印出每个结点的值。
思路
- 使用递归,打印第二个节点后再打印第一个节点
- 翻转链表再打印
- 使用栈
题解
- 使用递归
class Solution {
public:
vector<int> printListFromTailToHead(ListNode* head) {
vector<int> ans;
if(head == NULL) return ans;
ans = printListFromTailToHead(head->next);
ans.push_back(head->val);
return ans;
}
};
- 翻转链表
class Solution {
public:
vector<int> printListFromTailToHead(ListNode* head) {
vector<int> ans;
if (head == NULL) return ans;
ListNode *cur = head, **hd = &head;
while(cur->next != NULL){
ListNode *tmp = cur->next;
cur->next = tmp->next;
tmp->next = *hd;
*hd = tmp;
}
cur = *hd;
while(cur!=NULL){
ans.push_back(cur->val);
cur = cur->next;
}
return ans;
}
};
- 使用栈
class Solution {
public:
vector<int> printListFromTailToHead(ListNode* head) {
vector<int> ans;
if (head == NULL) return ans;
stack<int> st;
while(head != NULL){
st.push(head->val);
head = head->next;
}
while(!st.empty()){
ans.push_back(st.top());
st.pop();
}
return ans;
}
};
重建二叉树
二叉树, 递归
根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
思路
前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果,这里就能拿到左右子树的size,假设左子树size为leftlen,前序遍历中的idx+1到idx+leftlen都是左子树的前序遍历结果。然后分别对左右子树递归地求解。
题解
class Solution {
public:
TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
TreeNode* root = NULL;
solver(root, 0, 0, pre.size(), pre, vin);
return root;
}
private:
void solver(TreeNode*& root, int preIdx, int left, int leN, vector<int>& pre,vector<int>& vin){
if(leN<=0) return;
root = new TreeNode(pre[preIdx]);
int idx = find(vin.begin(), vin.end(), pre[preIdx]) - vin.begin();
int leNleft = idx-left;
int leNright = leN - leNleft - 1;
solver(root->left, preIdx+1, left, leNleft, pre, vin);
solver(root->right, preIdx+1+leNleft, idx+1, leNright, pre, vin);
}
};
- 拓展,已知后序和中序
class Solution {
public:
TreeNode* reConstructBinaryTree(vector<int> post,vector<int> vin) {
TreeNode* root = NULL;
solver(root, post.size()-1, 0, post.size(), post, vin);
return root;
}
private:
void solver(TreeNode*& root, int postIdx, int left, int leN, vector<int>& post,vector<int>& vin){
if(leN<=0) return;
root = new TreeNode(post[postIdx]);
int idx = find(vin.begin(), vin.end(), post[postIdx]) - vin.begin();
int leNleft = idx-left;
int leNright = leN - leNleft - 1;
solver(root->left, postIdx-1-leNright, left, leNleft, post, vin);
solver(root->right, postIdx-1, idx+1, leNright, post, vin);
}
};
用两个栈实现队列
栈,队列
用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。
思路
in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序。
题解
class Solution
{
public:
void push(int node) {
stack1.push(node);
}
int pop() {
if(stack2.empty()){
while(!stack1.empty()){
stack2.push(stack1.top());
stack1.pop();
}
}
int top = stack2.top();
stack2.pop();
return top;
}
private:
stack<int> stack1;
stack<int> stack2;
};
旋转数组的最小数字
数组,二分查找
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
思路
通过修改二分查找算法进行求解(l 代表 low,m 代表 mid,h 代表 high):
当 nums[m] <= nums[h] 时,表示 [m, h] 区间内的数组是非递减数组,[l, m] 区间内的数组是旋转数组,此时令 h = m;
否则 [m + 1, h] 区间内的数组是旋转数组,令 l = m + 1。
题解
class Solution {
public:
int minNumberInRotateArray(vector<int> rotateArray) {
int sz = rotateArray.size();
if (sz == 0) return 0;
int lo = 0, hi = sz-1;
while(lo<hi){
int mid = lo+(hi-lo)/2;
if(rotateArray[mid]<=rotateArray[hi]){
hi = mid;
}else{
lo = mid+1;
}
}
return rotateArray[lo];
}
};
- 拓展
如果数组元素允许重复,会出现一个特殊的情况:nums[l] == nums[m] == nums[h],此时无法确定解在哪个区间,需要切换到顺序查找。例如对于数组 {1,1,1,0,1},l、m 和 h 指向的数都为 1,此时无法知道最小数字 0 在哪个区间。
class Solution {
public:
int minNumberInRotateArray(vector<int> rotateArray) {
int sz = rotateArray.size();
if (sz == 0) return 0;
int lo = 0, hi = sz-1;
while(lo<hi){
int mid = lo+(hi-lo)/2;
if (rotateArray[lo] == rotateArray[mid] && rotateArray[mid] == rotateArray[hi]){
return minNumber(rotateArray, lo, hi);
}
if(rotateArray[mid]<=rotateArray[hi]){
hi = mid;
}else{
lo = mid+1;
}
}
return rotateArray[lo];
}
private:
int minNumber(vector<int>& nums, int l, int h){
for (int i = l; i < h; i++)
if (nums[i] > nums[i + 1])
return nums[i + 1];
return nums[l];
}
};
斐波那契数列
递归 动态规划
求斐波那契数列的第 n 项,n <= 39(从0开始,第0项为0)。
思路
递归或者动态规划,很容易想到,不过这里递归会超时,只能用空间换时间了
题解
class Solution {
public:
int Fibonacci(int n) {
mp = vector<int>(n+1, -1);
mp[0] = 0;
mp[1] = 1;
return solver(n);
}
private:
vector<int> mp;
int solver(int n){
if(mp[n]!=-1) return mp[n];
mp[n-2] = solver(n-2);
mp[n-1] = solver(n-1);
return mp[n] = mp[n-1]+mp[n-2];
}
};
跳台阶
动态规划
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
思路
典型的动态规划
题解
class Solution {
public:
int jumpFloor(int number) {
if(number == 0) return 1;
vector<int> ans(number+1);
ans[0] = 1;
ans[1] = 1;
for(int i = 2; i<=number; i++){
ans[i] = ans[i-1]+ans[i-2];
}
return ans[number];
}
};
- 当然也可以压缩一下空间复杂度
class Solution {
public:
int jumpFloor(int n) {
if (n <= 2)
return n;
int pre2 = 1, pre1 = 2;
int result = 0;
for (int i = 2; i < n; i++) {
result = pre2 + pre1;
pre2 = pre1;
pre1 = result;
}
return result;
}
};
变态跳台阶
动态规划 数学
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
思路
动态规划的思路和上个题目差不多
这题其实是道数学题,已知递推公式求解a(n),会发现a(n)其实是一个等比数列
题解
class Solution {
public:
int jumpFloorII(int number) {
if(number == 0) return 1;
vector<int> ans(number+1);
ans[0] = 1;
ans[1] = 1;
for(int i = 2; i<=number; i++){
for(int j = 0; j<i; j++){
ans[i]+=ans[j];
}
}
return ans[number];
}
};
class Solution {
public:
int jumpFloorII(int number) {
if (number == 0) return 1;
return pow(2, number-1);
}
};
矩形覆盖
动态规划
我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
思路
这也是一个典型的动态规划,注意初始化
题解
class Solution {
public:
int rectCover(int n) {
if(n == 0) return 0;
vector<int> ans(n+1, 1);
for(int i = 2; i<=n; i++){
ans[i] = ans[i-1]+ans[i-2];
}
return ans[n];
}
};
- 压缩空间
class Solution {
public:
int rectCover(int n) {
if (n <= 2)
return n;
int pre2 = 1, pre1 = 2;
int result = 0;
for (int i = 3; i <= n; i++) {
result = pre2 + pre1;
pre2 = pre1;
pre1 = result;
}
return result;
}
};
二进制中 1 的个数
位运算
输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
思路
-
比较容易想到的是,用贪心算法,从尾到头count。
-
考虑n&(n-1)
该位运算去除 n 的位级表示中最低的那一位。
n : 10110100 n-1 : 10110011 n&(n-1) : 10110000
时间复杂度:O(M),其中 M 表示 1 的个数。
题解
class Solution {
public:
int NumberOf1(int n) {
int count=0;
unsigned int flag=1;
while(flag){
if (n & flag){
count++;
}
flag=flag<<1;
}
return count;
}
};
class Solution {
public:
int NumberOf1(int n) {
int cnt = 0;
while (n != 0) {
cnt++;
n &= (n - 1);
}
return cnt;
}
};
调整数组顺序使奇数位于偶数前面
数组
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
思路
- 创建一个新的数组,先统计一下原数组的奇数数量便可以得到第一个偶数的下标,然后根据奇偶依次填入新的数组。
in-place
的贪心写法,找到一个偶数,继续往后找直到找到一个奇数,把这个奇数提到前面来。
题解
- 开辟新数组的写法
class Solution {
public:
void reOrderArray(vector<int> &array) {
int sz = array.size();
if (sz <= 1) return;
vector<int> ans(array.begin(), array.end());
int oddct = 0;
for(int i = 0; i<sz; i++){
if((array[i]&1) == 1) oddct++;
}
int j = 0;
for(int i = 0; i<sz; i++){
if((ans[i]&1) == 1) array[j++] = ans[i];
else array[oddct++] = ans[i];
}
}
};
in-place
的写法
class Solution {
public:
void reOrderArray(vector<int> &array) {
int sz = array.size();
if (sz <= 1) return;
int i = 0;
while(i != sz-1){
if((array[i]&1)==1) {// 奇数跳过
i++;
continue;
}
int j = i+1;
while(j != sz && (array[j]&1)==0){
j++;
}
if(j == sz) return;// 后面没奇数了
int tmp = array[j];
while(j!=i){
array[j] = array[j-1];
j--;
}
array[i] = tmp;
i++;
}
}
};
链表中倒数第 K 个结点
链表
输入一个链表,输出该链表中倒数第k个结点。
思路
-
可以简单先统计一下链表的长度,然后移动。
-
设链表的长度为 N。设置两个指针 fast 和 slow,先让 fast 移动 K-1 个节点。此时让 fast 和 slow 同时移动,可以知道当 fast 移动到链表结尾时,slow 移动到第 N - K 个节点处,该位置就是倒数第 K 个节点。
题解
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
unsigned int num = 0;
ListNode* tmp = pListHead;
while(tmp){
num++;
tmp = tmp->next;
}
if(k>num) return NULL;
unsigned int c = num-k;
while(c--){
pListHead = pListHead->next;
}
return pListHead;
}
};
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, int k) {
if(pListHead == NULL) return NULL;
ListNode* fast = pListHead, *slow = pListHead;
while(fast!=NULL && --k){
fast = fast->next;
}
if(fast == NULL) return NULL;
while(fast->next != NULL){
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
合并两个排序的链表
链表
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
思路
- 递归思路
- 非递归思路
题解
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
if(pHead1 == NULL) return pHead2;
if(pHead2 == NULL) return pHead1;
if(pHead1->val<=pHead2->val){
pHead1->next = Merge(pHead1->next, pHead2);
return pHead1;
}else{
pHead2->next = Merge(pHead1, pHead2->next);
return pHead2;
}
}
};
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
if(pHead1 == NULL) return pHead2;
if(pHead2 == NULL) return pHead1;
ListNode guard(-1);
ListNode *cur = &guard;
while(pHead1!=NULL && pHead2!=NULL){
if(pHead1->val<=pHead2->val){
cur->next = pHead1;
pHead1 = pHead1->next;
}else{
cur->next = pHead2;
pHead2 = pHead2->next;
}
cur = cur->next;
}
if(pHead1!=NULL){
cur->next = pHead1;
}
if(pHead2!=NULL){
cur->next = pHead2;
}
return guard.next;
}
};
树的子结构
二叉树 递归
输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
思路
第一步在树A中找到和树B的根节点的值一样的节点R,第二步再判断树A中以R为根节点的子树是不是包含和树B一样的结构。
题解
class Solution {
public:
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2)
{
if(pRoot2 == NULL || pRoot1 == NULL) return false;
return HasSubtreeSolver(pRoot1, pRoot2) || HasSubtree(pRoot1->left, pRoot2) ||
HasSubtree(pRoot1->right, pRoot2);
}
private:
bool HasSubtreeSolver(TreeNode* pRoot1, TreeNode* pRoot2){
if(pRoot2 == NULL) return true;// root2搜完了还没返回false则返回true
if(pRoot1 == NULL && pRoot2 != NULL) return false;// root1搜完了root2还没搜完
if(pRoot1->val != pRoot2->val) return false;
return HasSubtreeSolver(pRoot1->left, pRoot2->left)&&
HasSubtreeSolver(pRoot1->right, pRoot2->right);
}
};
二叉树的镜像
二叉树 递归
操作给定的二叉树,将其变换为源二叉树的镜像。
二叉树的镜像定义:源二叉树
8
/ \
6 10
/ \ / \
5 7 9 11
镜像二叉树
8
/ \
10 6
/ \ / \
11 9 7 5
思路
递归求解
题解
class Solution {
public:
void Mirror(TreeNode *pRoot) {
pRoot = Solver(pRoot);
}
private:
TreeNode* Solver(TreeNode *pRoot){
if(pRoot == NULL) return NULL;
if (pRoot->left == NULL && pRoot->right == NULL) return pRoot;
TreeNode* left = Solver(pRoot->left);
TreeNode* right = Solver(pRoot->right);
pRoot->left = right;
pRoot->right = left;
return pRoot;
}
};
顺时针打印矩阵
二维数组
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
思路
四个角设立四个标志位,按照模拟的方法贪心求解
题解
class Solution {
public:
vector<int> printMatrix(vector<vector<int> > matrix) {
vector<int> ans;
int R = matrix.size();
if (R == 0) return ans;
int C = matrix[0].size();
if (C == 0) return ans;
int Rlo = 0, Rhi = R - 1, Clo = 0, Chi = C - 1;
while(true){
if(Chi<Clo) return ans;
for(int i = Clo; i<=Chi; i++){
ans.push_back(matrix[Rlo][i]);
}
if(Rlo+1>Rhi) return ans;
for(int i = Rlo+1; i <= Rhi; i++){
ans.push_back(matrix[i][Chi]);
}
if(Chi-1<Clo) return ans;
for(int i = Chi-1; i>=Clo; i--){
ans.push_back(matrix[Rhi][i]);
}
if(Rhi-1<Rlo+1) return ans;
for(int i = Rhi-1;i>=Rlo+1;i--){
ans.push_back(matrix[i][Clo]);
}
Rlo++;Clo++;
Rhi--;Chi--;
}
return ans;
}
};
包含 min 函数的栈
栈
定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。
思路
每次更新min时再压栈一次
题解
class Solution {
public:
void push(int value) {
if(minn>value){
st.push(minn);
minn = value;
}
st.push(value);
}
void pop() {
if(st.top() == minn){
st.pop();
minn = st.top();
}
st.pop();
}
int top() {
return st.top();
}
int min() {
return minn;
}
private:
int minn = INT_MAX;
stack<int> st;
};
栈的压入、弹出序列
栈
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)
思路
使用一个栈来模拟压入弹出操作
题解
class Solution {
public:
bool IsPopOrder(vector<int> pushV,vector<int> popV) {
int sz = pushV.size();
if(sz != popV.size()) return false;
stack<int> st;
int i =0, j = 0;
while(j!=sz){
if(pushV[i] == popV[j]){
j++;
i++;
}else if(!st.empty()&&st.top() == popV[j]){
st.pop();
j++;
}else if(i<=sz){
st.push(pushV[i++]);
}else{
return false;
}
}
return true;
}
};
从上往下打印二叉树
二叉树 层次遍历
从上往下打印出二叉树的每个节点,同层节点从左至右打印。
例如,以下二叉树层次遍历的结果为:1,2,3,4,5,6,7
题解
- 就是层序遍历二叉树
class Solution {
public:
vector<int> PrintFromTopToBottom(TreeNode* root) {
vector<int> ans;
if(root == NULL) return ans;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
TreeNode* fro = q.front();
q.pop();
ans.push_back(fro->val);
if(fro->left) q.push(fro->left);
if(fro->right) q.push(fro->right);
}
return ans;
}
};
二叉搜索树的后序遍历序列
二叉搜索树 后序遍历 递归
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。假设输入的数组的任意两个数字都互不相同。
例如,下图是后序遍历序列 1,3,2 所对应的二叉搜索树。
题解
- 考虑找右子树的后序遍历起始点:从开始正向遍历找到第一个大于根节点的即是右子树的起始点。
- 考虑搜索树的特性:右子树所有点的值都大于根节点的值,不满足则false。
- 递归判断左右子树。
class Solution {
public:
bool VerifySquenceOfBST(vector<int> sequence) {
int sz = sequence.size();
if(sz == 0) return false;
if(sz < 3) return true;
return solver(sequence, 0, sz-1);
}
private:
bool solver(vector<int> seq, int start, int end){
if(start>=end) return true;
int r = start;
for(;r<end;r++){
if(seq[r] > seq[end]) break;// 找到右子树后序的开始点
}
for(int j = r; j<end; j++){
if(seq[j] < seq[end]) return false;
}
return solver(seq, start, r-1)&&solver(seq, r, end-1);
}
};
二叉树中和为某一值的路径
二叉树 dfs
输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前)
下图的二叉树有两条和为 22 的路径:10, 5, 7 和 10, 12
题解
- 注意审题,路径表示根到叶,非任意两节点,用dfs回溯轻松解决
class Solution {
public:
vector<vector<int> > FindPath(TreeNode* root,int expectNumber) {
vector<vector<int>> ans;
if(root == NULL) return ans;
vector<int> path;
solver(root, expectNumber, ans, path);
sort(ans.begin(), ans.end(), [](const auto& i, const auto& j){
return i.size()>j.size();
});
return ans;
}
private:
void solver(TreeNode* cur, int expectNumber, vector<vector<int>>& ans, vector<int>& path){
if(cur == NULL) return;
expectNumber -= cur->val;
if(expectNumber < 0) return;
path.push_back(cur->val);
if(expectNumber == 0 && cur->left == NULL && cur->right == NULL){
ans.push_back(path);
}else{
solver(cur->left, expectNumber, ans, path);
solver(cur->right, expectNumber, ans, path);
}
path.pop_back();
}
};
拓展
问:求自顶向下所有path的数量
思路:用一个map记录从根到该节点的path数量
class Solution {
unordered_map<int, int> NodeSum;
int Tsum;
int ans;
public:
int pathSum(TreeNode* root, int sum) {
Tsum = sum;
ans = 0;
NodeSum[0] = 1;
DFS(root, 0);
return ans;
}
void DFS(TreeNode* root, int sum){
if(root==NULL) return;
sum+=root->val;
ans += NodeSum[sum - Tsum];
NodeSum[sum]++;
DFS(root->left, sum);
DFS(root->right, sum);
NodeSum[sum]--;
}
};
复杂链表的复制
链表 map
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
题解
用一个map记录原链表各个节点到clone链表各个节点的映射。
class Solution {
public:
RandomListNode* Clone(RandomListNode* pHead)
{
if (pHead == NULL) return NULL;
RandomListNode* hd = pHead;
while(hd != NULL){
if(!mp.count(hd)){
mp[hd] = new RandomListNode(hd->label);
}
if(hd->next){
if(!mp.count(hd->next)) mp[hd->next] = new RandomListNode(hd->next->label);
mp[hd]->next = mp[hd->next];
}
if(hd->random){
if(!mp.count(hd->random)) mp[hd->random] = new RandomListNode(hd->random->label);
mp[hd]->random = mp[hd->random];
}
hd = hd->next;
}
return mp[pHead];
}
private:
unordered_map<RandomListNode*, RandomListNode*> mp;
};
二叉搜索树与双向链表
二叉搜索树
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
题解
二叉搜索树的中序遍历是有序的,记录一下前后节点,可以递归也可以迭代
- 递归写法
class Solution {
public:
TreeNode* Convert(TreeNode* pRootOfTree)
{
inOrder(pRootOfTree);
return head;
}
private:
TreeNode* head = NULL;
TreeNode* pre = NULL;
void inOrder(TreeNode* root){
if(root == NULL) return;
inOrder(root->left);
root->left = pre;
if(pre!=NULL) pre->right = root;
pre = root;
if(head == NULL) head = root;
inOrder(root->right);
}
};
- 非递归写法
class Solution {
public:
TreeNode* Convert(TreeNode* pRootOfTree)
{
if (pRootOfTree == NULL) return NULL;
TreeNode* guard = new TreeNode(-1);
TreeNode* tmp = guard;
stack<TreeNode*> st;
TreeNode* cur = pRootOfTree;
while(!st.empty() || cur != NULL){
while(cur != NULL){
st.push(cur);
cur = cur->left;
}
cur = st.top();
st.pop();
tmp->right = cur;
cur->left = tmp;
tmp = cur;
cur = cur->right;
}
TreeNode* ans = guard->right;
ans->left = NULL;
return ans;
}
};
字符串的排列
字符串 递归 dfs 回溯
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串 abc,则打印出由字符 a, b, c 所能排列出来的所有字符串 abc, acb, bac, bca, cab 和 cba。
题解
全排列问题,最先想到的是dfs,这是一个可能有重复字符的dfs
class Solution {
public:
vector<string> Permutation(string str) {
int sz = str.size();
vector<string> ans;
if (sz == 0) return ans;
for(const auto& c:str){
charMp[c]++;
}
string tmp = "";
solver(sz, tmp, ans);
return ans;
}
private:
map<char, int> charMp;
void solver(int n, string& tmp, vector<string>& ans){
if(n == 0){
ans.push_back(tmp);
return;
}
for(auto& c:charMp){
if(c.second<=0) continue;
c.second--;
tmp.push_back(c.first);
solver(n-1, tmp, ans);
tmp.pop_back();
c.second++;
}
}
};
官方给的题解采用置换的方法
class Solution {
public:
vector<string> Permutation(string str) {
sz = str.size();
if(sz == 0) return vector<string>();
solver(str, 0);
return vector<string>(ans.begin(), ans.end());
}
private:
int sz;
set<string> ans;
void solver(string& str, int bg){
if(bg == sz) {
ans.insert(str);
return;
}
for(int i = bg; i<sz; i++){
swap(str[bg], str[i]);
solver(str, bg+1);
swap(str[bg], str[i]);
}
}
};
数组中出现次数超过一半的数字
数组
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
题解
- 这里简单的思路是开一个map记录一下数字出现次数,空间复杂度比较高。(算法略)
- 多数投票问题,可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题,比如在故障恢复时领导者选举便有应用,使得时间复杂度为 O(N)。
使用 cnt 来统计一个元素出现的次数,当遍历到的元素和统计元素相等时,令 cnt++,否则令 cnt–。如果前面查找了 i 个元素,且 cnt == 0,说明前 i 个元素没有 majority,或者有 majority,但是出现的次数少于 i / 2 ,因为如果多于 i / 2 的话 cnt 就一定不会为 0 。此时剩下的 n - i 个元素中,majority 的数目依然多于 (n - i) / 2,因此继续查找就能找出 majority。
class Solution {
public:
int MoreThanHalfNum_Solution(vector<int> numbers) {
int sz = numbers.size();
if(sz == 0) return 0;
int cnt = 0, maj = numbers[0];
for(int i = 0; i<sz; i++){
if(cnt == 0){
maj = numbers[i];
cnt++;
}else{
cnt = numbers[i] == maj?cnt+1:cnt-1;
}
if(cnt>sz/2) return maj;
}
cnt = 0;
for(int i = 0; i<sz; i++){
if(maj == numbers[i]) cnt++;
}
return cnt>sz/2?maj:0;
}
};
最小的 K 个数
堆 快速排序
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
题解
-
最容易想到的是nlog(n)排序取前k个数(算法略)
-
可以借助大根堆或者红黑树
- 复杂度:O(NlogK) + O(K)
- 特别适合处理海量数据
class Solution {
public:
vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
int sz = input.size();
vector<int> ans;
if(k>sz) return ans;
priority_queue<int, vector<int>, less<int>> pq;
for(int i = 0; i<sz; i++){
pq.push(input[i]);
if(pq.size()>k){
pq.pop();
}
}
while(!pq.empty()){
ans.push_back(pq.top());
pq.pop();
}
return ans;
}
};
- 采用快排的partition函数来做
- 复杂度:O(N) + O(1)
- 只有当允许修改数组元素时才可以使用
class Solution {
public:
vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
int sz = input.size();
vector<int> ans;
if (0 == sz || k >sz || k<=0) return ans;
int l = 0, r = sz-1;
int idx = partition(input, l, r);
while(idx+1 != k){
if(idx+1>k){
r = idx-1;
idx = partition(input, l, r);
}else{
l = idx+1;
idx = partition(input, l, r);
}
}
ans = vector<int>(input.begin(), input.begin()+k);
return ans;
}
private:
int partition(vector<int>& input, int l, int r){
if(l==r) return l;
int key = input[l];
while(l<r){
while(l<r && input[r]>=key) r--;
input[l] = input[r];
while(l<r && input[l]<=key) l++;
input[r] = input[l];
}
input[l] = key;
return l;
}
};
连续子数组的最大和
数组 动态规划
{6, -3, -2, 7, -15, 1, 2, 2},连续子数组的最大和为 8(从第 0 个开始,到第 3 个为止)。
题解
动态规划可以但没必要,太浪费空间了
class Solution {
public:
int FindGreatestSumOfSubArray(vector<int> array) {
int sz = array.size();
if(sz == 0) return 0;
int ans = INT_MIN;
vector<int> dp(sz+1, INT_MIN);
dp[1] = array[0];
for(int i = 2; i<=sz; i++){
dp[i] = max(dp[i-1]+array[i-1], array[i-1]);
ans = max(ans, dp[i]);
}
return ans;
}
};
注意题目要求是至少取一个元素,所以先更新ans
class Solution {
public:
int FindGreatestSumOfSubArray(vector<int> array) {
int ans = INT_MIN;
int summ = 0;
for(int i = 0; i<array.size();i++){
summ+=array[i];
ans = max(ans, summ);
if(summ < 0) summ = 0;
}
return ans;
}
};
从 1 到 n 整数中 1 出现的次数
位运算 动态规划
求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
题解
递归公式为dp[i] = dp[i/10]+末位是否为1
class Solution {
public:
int NumberOf1Between1AndN_Solution(int n)
{
vector<int> dp(n+1, 0);
int ans = 0;
for(int i = 1; i<=n; i++){
dp[i] = dp[i/10]+((i%10 == 1)?1:0);
ans+=dp[i];
}
return ans;
}
};
把数组排成最小的数
排序
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
题解
这是一道排序的题,需要重写sort函数的排序规则
class Solution {
public:
string PrintMinNumber(vector<int> numbers) {
sort(numbers.begin(), numbers.end(), [](const auto&i, const auto& j){
return (to_string(i)+to_string(j))<(to_string(j)+to_string(i));
});
string ss = "";
return accumulate(numbers.begin(), numbers.end(), ss, [](string _, int i){
return _+to_string(i);
});
}
};
丑数
数学
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
题解
用一个集合存放从小到大的丑数,因为新的丑数是由旧的丑数生成的,令三个指针p2,p3,p5分别指向乘法因子为235的旧丑数,如果某指针用过了,则该指针后移一位。
class Solution {
public:
int GetUglyNumber_Solution(int index) {
if(index == 0) return 0;
int p2 = 0, p3 = 0, p5 = 0;
vector<int> ans(index);
ans[0] = 1;
for(int i = 1; i<index; i++){
ans[i] = min(ans[p2]*2, min(ans[p3]*3, ans[p5]*5));
if(ans[i] == ans[p2]*2) p2++;
if(ans[i] == ans[p3]*3) p3++;
if(ans[i] == ans[p5]*5) p5++;
}
return ans[index-1];
}
};
第一个只出现一次的字符位置
字符串 map
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).
题解
class Solution {
public:
int FirstNotRepeatingChar(string s) {
vector<int> mp(256);
for(int i = 0; i < s.size(); ++i){
mp[s[i]]++;
}
for(int i = 0; i < s.size(); ++i){
if(mp[s[i]] == 1) return i;
}
return -1;
}
};
数组中的逆序对
数组 mergesort 搜索树
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
输入描述:
题目保证输入的数组中没有的相同的数字
数据范围:
对于%50的数据,size<=10^4
对于%75的数据,size<=10^5
对于%100的数据,size<=2*10^5
示例1
输入
1,2,3,4,5,6,7,0
输出
7
题解
利用归并排序算法,分成左右两个子集之后进行merge时,统计逆序对个数
class Solution {
public:
int InversePairs(vector<int> data) {
int sz = data.size();
if(sz == 0) return 0;
int ans = 0;
mergeSort(data, 0, sz, ans);
return ans;
}
private:
void mergeSort(vector<int>& nums, int low, int high, int &ans)
{
if(low+1 == high) return;
vector<int> temp;
int mid = (low+high)/2, right = mid;
mergeSort(nums, low, mid, ans);// [low, mid)
mergeSort(nums, mid, high, ans);// [mid, high)
int i = low, j = mid;
while(i<mid && j<high){
if(nums[i]<=nums[j]){
temp.push_back(nums[i++]);
}else{
temp.push_back(nums[j++]);
ans=(ans+(mid-i))%1000000007;
}
}
while(i<mid) temp.push_back(nums[i++]);
while(j<high) temp.push_back(nums[j++]);
for (i = 0; i < temp.size(); i++)
nums[low + i] = temp[i];
}
};
利用二叉搜索树,搜索树存一个比该节点大的节点数量,每次insert时更新
struct Node{
int larger, val;
Node* left, *right;
Node(int _val, int _larger):val(_val),
larger(_larger), left(NULL), right(NULL){
}
};
class Solution {
public:
int InversePairs(vector<int> data) {
int sz = data.size();
if(sz == 0) return 0;
int ct = 0;
Node* root = NULL;
for(int i = 0; i<sz; i++){
ct=(ct+insertTree(root, data[i]))%1000000007;
}
return ct;
}
private:
int insertTree(Node*& root, int val){
if(!root) return (root = new Node(val, 0)), 0;
if(root->val<val) return root->larger++, insertTree(root->right, val);
return insertTree(root->left, val)+root->larger+1;
}
};
两个链表的第一个公共结点
链表 map 数学
输入两个链表,找出它们的第一个公共结点。
题解
- 简单解法,用一个set记录一下某一个链表的所有node,从头遍历另一个链表
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
while(pHead1){
us.insert(pHead1);
pHead1 = pHead1->next;
}
while(pHead2){
if(us.count(pHead2)) return pHead2;
pHead2 = pHead2->next;
}
return pHead2;
}
private:
unordered_set<ListNode*> us;
};
- 考虑数学方法,设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。考虑分别指向两个链表头的指针p1和p2,这个等式表示的是两个指针走过步长为a+c+b时会在交点处相遇。
当p1访问到链表尾部时,令它从链表 B 的头部重新开始访问链表 B;同样地,当p2访问到链表尾部时,令它从链表 A 的头部重新开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
ListNode* p1 = pHead1, *p2 = pHead2;
while(p1 != p2){
p1 = p1?p1->next:pHead2;
p2 = p2?p2->next:pHead1;
}
return p1;
}
};
数字在排序数组中出现的次数
数组 二分搜索
统计一个数字在排序数组中出现的次数。
Input:
nums = 1, 2, 3, 3, 3, 3, 4, 6
K = 3
Output:
4
题解
当然可以直接遍历,最坏全访问一遍,这里是有序数组,很容易想到二分搜索
class Solution {
public:
int GetNumberOfK(vector<int> data ,int k) {
int sz = data.size();
if(sz == 0) return 0;
auto loit = lower_bound(data.begin(), data.end(), k);
if(loit == data.end() || *loit != k) return 0;
return upper_bound(loit, data.end(), k)-loit;
}
};
二叉树的深度
二叉树 递归
从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
题解
数据结构基础
class Solution {
public:
int TreeDepth(TreeNode* pRoot)
{
if(pRoot == NULL) return 0;
if(pRoot->left == NULL && pRoot->right == NULL) return 1;
return max(TreeDepth(pRoot->left)+1, TreeDepth(pRoot->right)+1);
}
};
平衡二叉树
树的遍历
输入一棵二叉树,判断该二叉树是否是平衡二叉树。
题解
判断左右子树的深度差是否<=1
class Solution {
public:
bool IsBalanced_Solution(TreeNode* pRoot) {
if(pRoot == NULL) return true;
if(pRoot->left == NULL && pRoot->right == NULL) return true;
bool ans = true;
helper(pRoot, ans);
return ans;
}
private:
int helper(TreeNode* pRoot, bool& ans){
if(pRoot == NULL) return 0;
if(pRoot->left == NULL && pRoot->right == NULL) return 1;
int l = helper(pRoot->left, ans)+1;
int r = helper(pRoot->right, ans)+1;
if(l>r){
if(l-r>1) ans = false;
}else{
if(r-l>1) ans = false;
}
return max(l,r);
}
};
数组里只出现一次的数字
数组 位运算
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
题解
两个不相等的元素在位级表示上必定会有一位存在不同,将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。
diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。
class Solution {
public:
void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
int diff= 0;
for(int i = 0; i<data.size(); i++){
diff ^= data[i];
}
diff &= -diff;
for(int i = 0; i<data.size(); i++){
if(diff&data[i]){
(*num1)^=data[i];
}else{
(*num2)^=data[i];
}
}
}
};
和为 S 的连续正数序列
滑动窗口
小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!
输出描述:
输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序
题解
先考虑一下上界,题目要求至少包含2个数,sum为奇数时,可用两个数相加,最大为sum/2+1;sum为偶数时,可用3个数相加,最大为sum/3+1。
窗口左指针初始为0,记录下窗口内的和summ,如果summ超了则左指针右滑。
class Solution {
public:
vector<vector<int> > FindContinuousSequence(int sum) {
int hi = 0;
if((sum &1) == 1){
hi = sum/2+1;
}else{
hi = sum/3+1;
}
vector<vector<int>> ans;
int lo = 1, summ = 0;
for(int j = 1; j<=hi; j++){
summ+=j;
while(j > lo+1 && summ>sum){// 窗口至少为2
summ-=lo;
lo++;
}
if((summ == sum) && (j-lo+1>=2)){
vector<int> tmp;
for(int i = lo; i<=j; i++){
tmp.push_back(i);
}
ans.push_back(tmp);
}
}
return ans;
}
};
左旋转字符串
字符串
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
题解
- 先将 “abc” 和 “XYZdef” 分别翻转,得到 “cbafedZYX”,然后再把整个字符串翻转得到 “XYZdefabc”。
class Solution {
public:
string LeftRotateString(string str, int n) {
int sz = str.size();
if(sz == 0 || n>=sz) return str;
reverse(str.begin(), str.begin()+n);
reverse(str.begin()+n, str.end());
reverse(str.begin(), str.end());
return str;
}
};
- 一种巧妙的in-place做法
参考右旋转k的图:
class Solution {
public:
string LeftRotateString(string str, int n) {
int sz = str.size();
if(sz == 0 || n>=sz) return str;
int ct = 0;
for(int i = sz-1; ct<sz; i--){
char tmp = str[i];
int ti = i;
do{
ti = ti-n;
if(ti < 0) ti += sz;
swap(tmp, str[ti]);
ct++;
}while(ti != i);
}
return str;
}
};
翻转单词顺序列
牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?
题解
这题从后往前贪心就好了
class Solution {
public:
string ReverseSentence(string str) {
string nstr;
int j = str.size()-1;
for(int i = str.size()-1; i>=0; i--){
while(j!=0 && str[j-1]!=' ') j--;
nstr+=(str.substr(j, i-j+1));
if(j!=0)
nstr+=' ';
i = j-1;
j = i;
}
return nstr;
}
};
扑克牌顺子
数组
LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张_)…他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子…LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。
题解
- 统计一下王的个数,用王去填充
class Solution {
public:
bool IsContinuous( vector<int> nums ) {
if (nums.size() < 5)
return false;
sort(nums.begin(), nums.end());
// 统计癞子数量
int cnt = 0;
for (int num : nums)
if (num == 0)
cnt++;
// 使用癞子去补全不连续的顺子
for (int i = cnt; i < nums.size() - 1; i++) {
if (nums[i + 1] == nums[i])
return false;
cnt -= nums[i + 1] - nums[i] - 1;
}
return cnt >= 0;
}
};
- 用一下红黑树
class Solution {
public:
bool IsContinuous( vector<int> numbers ) {
if(numbers.size()!=5) return false;
multiset<int> ms;
int wsz = 0;
for(int i = 0; i<5; i++){
if(numbers[i] == 0) wsz++;
else{
if(ms.count(numbers[i])) return false;
else ms.insert(numbers[i]);
}
}
return *ms.rbegin() - (*ms.begin()) + 1 <= ms.size() + wsz;
}
};
孩子们的游戏
循环链表 数学
每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0…m-1报数…这样下去…直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!_)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)
如果没有小朋友,请返回-1
题解
经典的约瑟夫环问题。
- 采用循环链表的方法
class Solution {
struct cNode{
int no;
cNode* nxt;
cNode(int _no):no(_no), nxt(NULL){
}
};
public:
int LastRemaining_Solution(int n, int m)
{
if(n == 0) return -1;
cNode* cur = new cNode(0);
cNode* tmp = cur;
for(int i = 1; i<n; i++){
tmp->nxt = new cNode(i);
tmp = tmp->nxt;
}
tmp->nxt = cur;
while(tmp->nxt!=tmp){
int mtmp = m-1;
while(mtmp--){
tmp = tmp->nxt;
}
tmp->nxt = tmp->nxt->nxt;
}
return tmp->no;
}
};
- 递归方法
class Solution {
public:
int LastRemaining_Solution(int n, int m)
{
if(n == 0) return -1;
if(n == 1) return 0;
return (LastRemaining_Solution(n-1, m)+m)%n;
}
};
- 数学方法
class Solution {
public:
int LastRemaining_Solution(int n, int m)
{
if(n==0) return -1;
int s=0;
for(int i=2;i<=n;i++){
s=(s+m)%i;
}
return s;
}
};
求 1+2+3+…+n
数学 递归
求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
题解
使用递归解法最重要的是指定返回条件,但是本题无法直接使用 if 语句来指定返回条件。
条件与 && 具有短路原则,即在第一个条件语句为 false 的情况下不会去执行第二个条件语句。利用这一特性,将递归的返回条件取非然后作为 && 的第一个条件语句,递归的主体转换为第二个条件语句,那么当递归的返回条件为 true 的情况下就不会执行递归的主体部分,递归返回。
本题的递归返回条件为 n <= 0,取非后就是 n > 0;递归的主体部分为 sum += Sum_Solution(n - 1),转换为条件语句后就是 (sum += Sum_Solution(n - 1)) > 0。
class Solution {
public:
int Sum_Solution(int n) {
int sum=n;
sum>0 && (sum+=Sum_Solution(n-1));
return sum;
}
};
不用加减乘除做加法
位运算 数学
写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。
题解
a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。
递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。
class Solution {
public:
int Add(int num1, int num2)
{
return num2 ? Add(num1^num2, (num1&num2)<<1) : num1;
}
};
把字符串转换成整数
将一个字符串转换成一个整数,字符串不是一个合法的数值则返回 0,要求不能使用字符串转换整数的库函数。
Iuput:
+2147483647
1a33
Output:
2147483647
0
题解
class Solution {
public:
int StrToInt(string str) {
const int length = str.length();
int isNegtive = 1, overValue = 0;
int digit = 0, value = 0;
if (length == 0) return 0;
else {
int idx = 0;
if (str[0] == '-') { isNegtive = -1; idx = 1;}
else if (str[0] == '+') {idx = 1;}
for (; idx<length; idx++) {
digit = str[idx]-'0';
// overValue表示本轮循环是否会越界
overValue = isNegtive*value - INT_MAX/10
+ (((isNegtive+1)/2 + digit > 8) ? 1:0);
if (digit<0 || digit>9) return 0;
else if (overValue > 0) return 0;
value = value*10 + isNegtive*digit;
}
return value;
}
}
};
数组中重复的数字
数组
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
题解
- 用set,空间复杂度比较高
class Solution {
public:
// Parameters:
// numbers: an array of integers
// length: the length of array numbers
// duplication: (Output) the duplicated number in the array number
// Return value: true if the input is valid, and there are some duplications in the array number
// otherwise false
bool duplicate(int numbers[], int length, int* duplication) {
unordered_set<int> us;
for(int i = 0; i<length; i++){
if (us.count(numbers[i])){
*duplication = numbers[i];
return true;
}
us.insert(numbers[i]);
}
return false;
}
};
-
排序,时间复杂度比较高
-
in-place置换算法
class Solution {
public:
bool duplicate(int nums[], int length, int* duplication) {
for(int i = 0; i<length; i++){
while(nums[i]!=i){
if(nums[i] == nums[nums[i]]){
*duplication = nums[i];
return true;
}
swap(nums[i],nums[nums[i]]);
}
}
return false;
}
};
构建乘积数组
给定一个数组A[0,1,…,n-1],请构建一个数组B[0,1,…,n-1],其中B中的元素B[i]=A[0]*A[1]*…*A[i-1]*A[i+1]*…*A[n-1]。不能使用除法。
题解
将乘积拆分为左右,通过左右两次遍历得到结果
class Solution {
public:
vector<int> multiply(const vector<int>& A) {
int len=A.size();
vector<int> B(len);
int res=1;
for(int i=0;i<len;i++){
B[i]=res;
res*=A[i];
}
res=1;
for(int i=len-1;i>=0;i--){
B[i]*=res;
res*=A[i];
}
return B;
}
};
正则表达式匹配
请实现一个函数用来匹配包括’.‘和’*‘的正则表达式。模式中的字符’.‘表示任意一个字符,而’*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配
题解
动态规划
class Solution {
public:
bool match(char* str, char* pattern)
{
int sz1 = strlen(str);
int sz2 = strlen(pattern);
vector<vector<bool>> ans(sz1+1, vector<bool>(sz2+1));
ans[0][0] = true;
for(int i = 2; i<=sz2; i++){
ans[0][i] = ans[0][i-2] && pattern[i-1] == '*';
}
for(int i = 1; i<=sz1; i++){
for(int j = 1; j<=sz2; j++){
if(pattern[j-1] == '*'){
ans[i][j] = ans[i][j-2] || (ans[i-1][j] && (pattern[j-2] == '.' || str[i-1] == pattern[j-2]));
}else{
ans[i][j] = ans[i-1][j-1] && (pattern[j-1] == '.' || str[i-1] == pattern[j-1]);
}
}
}
return ans[sz1][sz2];
}
};
字符流中第一个不重复的字符
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
输出描述:
如果当前字符流没有存在出现一次的字符,返回#字符。
题解
class Solution {
public:
Solution(){
it = 0;
}
//Insert one char from stringstream
void Insert(char ch)
{
s+=ch;
um[ch]++;
while(um[s[it]]>1&&it!=s.size()){
it++;
}
}
//return the first appearence once char in current stringstream
char FirstAppearingOnce()
{
if(it>s.size()-1) return '#';
return s[it];
}
private:
unordered_map<char, int> um;
string s;
int it;
};
链表中环的入口节点
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
题解
使用双指针,一个快指针 fast 每次移动两个节点,一个慢指针 slow 每次移动一个节点。因为存在环,所以两个指针必定相遇在环中的某个节点上。
假设环入口节点为 y1,相遇所在节点为 z1。
假设快指针 fast 在圈内绕了 N 圈,则总路径长度为 x+Ny+(N-1)z。z 为 (N-1) 倍是因为快慢指针最后已经在 z1 节点相遇了,后面就不需要再走了。
而慢指针 slow 总路径长度为 x+y。
因为快指针是慢指针的两倍,因此 x+Ny+(N-1)z = 2(x+y)。
我们要找的是环入口节点 y1,也可以看成寻找长度 x 的值,因此我们先将上面的等值分解为和 x 有关:x=(N-2)y+(N-1)z。
上面的等值没有很强的规律,但是我们可以发现 y+z 就是圆环的总长度,因此我们将上面的等式再分解:x=(N-2)(y+z)+z。这个等式左边是从起点x1 到环入口节点 y1 的长度,而右边是在圆环中走过 (N-2) 圈,再从相遇点 z1 再走过长度为 z 的长度。此时我们可以发现如果让两个指针同时从起点 x1 和相遇点 z1 开始,每次只走过一个距离,那么最后他们会在环入口节点相遇。
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
if(pHead == NULL || pHead->next == NULL) return NULL;
ListNode* fast = pHead, *slow = pHead;
while(fast->next != NULL && fast != NULL){
fast = fast->next->next;
slow = slow->next;
if(slow == fast) break;// 快慢指针相遇
}
if(fast == NULL || fast->next == NULL) return NULL;//说明无环,走到了空节点
slow = pHead;
while(fast != slow){
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
删除链表中重复的节点
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
题解
- 递归写法
class Solution {
public:
ListNode* deleteDuplication(ListNode* pHead)
{
if(pHead == NULL|| pHead->next == NULL) return pHead;
ListNode* next = pHead->next;
if(next->val == pHead->val){
while(next!=NULL && next->val==pHead->val){
next = next->next;
}
return deleteDuplication(next);
}else{
pHead->next = deleteDuplication(pHead->next);
return pHead;
}
}
};
- 迭代写法,维护一个back节点,记录待删除节点的前一节点,cur记录当前节点,如果有重复,则删除cur节点后的重复节点,然后再删除cur节点。
class Solution {
public:
ListNode* deleteDuplication(ListNode* pHead)
{
if(pHead == NULL|| pHead->next == NULL) return pHead;
ListNode* guard = new ListNode(-1);
ListNode* back = guard;
back->next = pHead;
ListNode* cur = pHead, *tmp = cur->next;
while(tmp){
bool ok = false;
while(tmp!=NULL && tmp->val == cur->val){
ok = true;
cur->next = tmp->next;
tmp = cur->next;
}
if(ok){
back->next = cur->next;
}else{
back = back->next;
}
cur = back->next;
if(cur == NULL) return guard->next;
tmp = cur->next;
}
return guard->next;
}
};
二叉树的下一节点
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
题解
class Solution {
public:
TreeLinkNode* GetNext(TreeLinkNode* pNode)
{
//if(pNode == NULL || (pNode->left == NULL && pNode->right == NULL)) return NULL;
// 1. pNode含有右子树的情况
if(pNode->right){// 找右子树的最左
TreeLinkNode* tmp = pNode->right;
while(tmp->left){
tmp = tmp->left;
}
return tmp;
}
// 2. pNode是根节点,且没有右子树
if(pNode->next == NULL) return NULL;
// 3. pNode是其父节点的左子节点
if(pNode->next->left == pNode){
return pNode->next;
}
// 4. pNode是其父节点的右子节点,往上找,找到父节点的左子节点是tmp的情况
TreeLinkNode* tmp = pNode->next;
while(tmp->next != NULL && tmp->next->left != tmp){
tmp = tmp->next;
}
return tmp->next;
}
};
对称的二叉树
请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。
题解
class Solution {
public:
bool isSymmetrical(TreeNode* pRoot)
{
if (pRoot == NULL || (pRoot->left == NULL && pRoot->right == NULL))
return true;
return isSymTree(pRoot->left, pRoot->right);
}
private:
bool isSymTree(TreeNode* l, TreeNode* r){
if(l == NULL && r == NULL) return true;
if(l == NULL || r == NULL) return false;
if(l->val != r->val) return false;
return isSymTree(l->left, r->right) && isSymTree(l->right, r->left);
}
};
按之字形打印二叉树
请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
题解
- 层序遍历,用一个信号量记录正序还是反序,反序reverse一下该层的结果
class Solution {
public:
vector<vector<int>> Print(TreeNode* pRoot) {
vector<vector<int>> ans;
if(pRoot == NULL) return ans;
queue<TreeNode*> q;
q.push(pRoot);
bool f = false;
while(!q.empty()){
int sz = q.size();
vector<int> subAns;
while(sz--){
TreeNode* fro = q.front();
q.pop();
subAns.push_back(fro->val);
if(fro->left) q.push(fro->left);
if(fro->right) q.push(fro->right);
}
if(f) reverse(subAns.begin(), subAns.end());
ans.push_back(subAns);
f = (!f);
}
return ans;
}
};
- 使用stack
class Solution {
public:
vector<vector<int>> Print(TreeNode* pRoot) {
vector<vector<int>> ans;
if(pRoot == NULL) return ans;
queue<TreeNode*> q;
q.push(pRoot);
stack<TreeNode*> st;
bool f = true;
while(!q.empty()){
int sz = q.size();
vector<int> subAns;
while(sz--){
TreeNode* fro = q.front();
q.pop();
subAns.push_back(fro->val);
if (f){
if(fro->left) st.push(fro->left);
if(fro->right) st.push(fro->right);
}else{
if(fro->right) st.push(fro->right);
if(fro->left) st.push(fro->left);
}
}
ans.push_back(subAns);
f = (!f);
while(!st.empty()){
q.push(st.top());
st.pop();
}
}
return ans;
}
};
把二叉树打印成多行
从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
题解
class Solution {
public:
vector<vector<int> > Print(TreeNode* pRoot) {
vector<vector<int>> ans;
if(pRoot == NULL) return ans;
queue<TreeNode*> q;
q.push(pRoot);
while(!q.empty()){
int sz = q.size();
vector<int> subAns;
while(sz--){
TreeNode* fro = q.front();
q.pop();
subAns.push_back(fro->val);
if (fro->left) q.push(fro->left);
if (fro->right) q.push(fro->right);
}
ans.push_back(subAns);
}
return ans;
}
};
序列化二叉树
请实现两个函数,分别用来序列化和反序列化二叉树
二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),以 ! 表示一个结点值的结束(value!)。
二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。
题解
ostream& operator<<(ostream& os, TreeNode* node) {
if (node != nullptr) {
os << node->val << " ";
os << node->left;
os << node->right;
} else {
os << "# ";
}
return os;
}
istream& operator>>(istream& is, TreeNode*& node) {
string next;
if (!(is >> next)) {
return is;
}
if (next == "#") {
node = nullptr;
}else{
node = new TreeNode(stoi(next));
is >> node->left;
is >> node->right;
}
return is;
}
class Solution {
public:
char* Serialize(TreeNode *root)
{
stringstream ss;
ss << root;
string sss = ss.str();
char *ret = new char[sss.length() + 1];
strcpy(ret, const_cast<char*>(sss.c_str()));
return ret;
}
TreeNode* Deserialize(char *str) {
stringstream ss(str);
TreeNode* root;
ss >> root;
return root;
}
};
二叉搜索树的第k个节点
给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。
题解
二叉搜索树的中序遍历是有序的
class Solution {
public:
TreeNode* KthNode(TreeNode* pRoot, int k)
{
if(pRoot == NULL || k<=0) return NULL;
stack<TreeNode*> st;
TreeNode* cur = pRoot;
while(!st.empty() || cur != NULL){
while(cur!=NULL){
st.push(cur);
cur = cur->left;
}
cur = st.top();
st.pop();
if(--k == 0){
return cur;
}
cur = cur->right;
}
return NULL;
}
};
数据流中的中位数
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
题解
可以用一个红黑树保持有序
class Solution {
public:
Solution(){
sz = 0;
}
void Insert(int num)
{
holder.insert(num);
sz++;
}
double GetMedian()
{
if(sz == 0) return 0;
int hsz = sz/2;
auto f = holder.begin();
if(sz&1){// 奇数
while(hsz--){
f = next(f);
}
return *f;
}else{
while(--hsz){
f = next(f);
}
return (double)(*f+*next(f))/2;
}
}
private:
multiset<int> holder;
int sz;
};
滑动窗口的最大值
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
题解
用一个有序的deque存储最值,队头到队尾降序排列
class Solution {
public:
vector<int> maxInWindows(const vector<int>& num, int size)
{
vector<int> ans;
int sz = num.size();
if(sz == 0 || size == 0 || size > sz) return ans;
deque<int> dq;
for(int i = 0; i<size; i++){
while(!dq.empty() && num[dq.back()]<=num[i]) dq.pop_back();
// 清理掉队尾比num[i]还小的,
//因为如果num[i]如果淘汰了,比他小的且在他前面的肯定也会淘汰
dq.push_back(i);
}
ans.push_back(num[dq.front()]);
for(int i = size; i < num.size(); i++){
// 清理不在窗口内的元素
while(!dq.empty() && dq.front()<i-size+1) dq.pop_front();
// 清理队尾
while(!dq.empty() && num[dq.back()]<=num[i]) dq.pop_back();
dq.push_back(i);
ans.push_back(num[dq.front()]);
}
return ans;
}
};
矩阵中的路径
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
题解
这题典型的dfs+回溯法
class Solution {
public:
bool hasPath(char* matrix, int rows, int cols, char* str)
{
strL = strlen(str);
if (rows == 0 || cols == 0 || strL == 0) return false;
for(int i = 0; i<rows; i++){
for(int j = 0; j<cols; j++){
if(solver(i, j, rows, cols, matrix, 0, str)) return true;
}
}
return false;
}
private:
int strL;
bool solver(int i, int j, int rows, int cols, char* matrix, int idx, char* str){
if(i<0 || i>=rows || j<0 || j>=cols || matrix[i*cols+j] == '*') return false;
char cur = matrix[i*cols+j];
if (cur != str[idx]) return false;
if (idx == strL-1) return true;
matrix[i*cols+j] = '*';
if(solver(i+1, j, rows, cols, matrix, idx+1, str)) return true;
if(solver(i-1, j, rows, cols, matrix, idx+1, str)) return true;
if(solver(i, j+1, rows, cols, matrix, idx+1, str)) return true;
if(solver(i, j-1, rows, cols, matrix, idx+1, str)) return true;
matrix[i*cols+j] = cur;
return false;
}
};
机器人的运动范围
地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
题解
用一个flag数组标记走过的格子
class Solution {
public:
int movingCount(int threshold, int rows, int cols)
{
bool *flag = new bool[rows * cols];
for(int i = 0; i < rows * cols; i++)
flag[i] = false;
int count = moving(threshold, rows, cols, 0, 0, flag);
return count;
}
private:
int moving(int threshold, int rows, int cols, int i, int j, bool* flag)
{
int count = 0;
if(i >= 0 && i < rows && j >= 0 && j < cols && getsum(i) + getsum(j) <= threshold && flag[i * cols + j] == false)
{
flag[i * cols + j] = true;
count =1 + moving(threshold, rows, cols, i + 1, j, flag)
+ moving(threshold, rows, cols, i - 1, j, flag)
+ moving(threshold, rows, cols, i , j - 1, flag)
+ moving(threshold, rows, cols, i, j + 1, flag);
}
return count;
}
int getsum(int num)
{
int sum = 0;
while(num)
{
sum += num % 10;
num /= 10;
}
return sum;
}
};
剪绳子
给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],…,k[m]。请问k[0]xk[1]x…xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
- 输入描述:
输入一个数n,意义见题面。(2 <= n <= 60) - 输出描述:
输出答案。
示例1
- 输入
8 - 输出
18
题解
- 分成i段时,均分肯定乘积最大,最多分number/2段,因为再多就会出现长度为1的段。
class Solution {
public:
int cutRope(int number) {
int ans = 1;
if(number == 3) return 2;
for(int i = 2; i<=number/2; i++){
ans = max(ans, calc(number, i));
}
return ans;
}
private:
int calc(int number, int i){
int n = number/i;
int ans = 1;
while(--i){
ans*=n;
number-=n;
}
ans*=number;
return ans;
}
};
- 动态规划
class Solution {
public:
int cutRope(int number) {
vector<int> ans(number+1);
ans[1] = 1;
for(int i = 2; i<=number; i++){
for(int j = 1; j<i; j++){
// 因为至少切一刀,所以要考虑整段的情况
ans[i] = max(ans[i], max(ans[j]*(i-j), j*(i-j)));
}
}
return ans[number];
}
};