之前一直在用C刷题,但很多题目明显麻烦,也导致自己对leetcode比较抗拒,又懒于学习C++,java之类的,所以刷题一直拖着,现在终于下定决心用C++来刷题,发现真的好用,很多东西有现成的,可以更专注于学习算法,因此希望可以坚持下去。不会的题都是参考leetcode题解大佬Krahets的。
day1
双栈实现队列、包含min的栈
首先把C++中这些类常用的函数记录一下。
#include <stack>//栈
stack<int> st;
st.push(elem);
st.pop();
st.emplace(args);//用参数args构造一个元素并添加进栈顶
st.top();
st.empty();
st.size();
对于双栈实现队列,只需要设置两个栈A,B,进栈只需入栈A,出栈时,如果B栈非空,则popB栈,若A,B都空,返回-1,如果B为空,则将A中元素出栈入B栈,再popB栈。
class CQueue {
public:
stack<int> inStack, outStack;//in是入栈、out是出栈
CQueue() {
}
void appendTail(int value) {//整体入队则直接在push入in栈
inStack.push(value);
}
int deleteHead() {
if(!outStack.empty()){//若out非空,则记录栈头元素再出栈
int value = outStack.top();
outStack.pop();
return value;
}
if(inStack.empty()) return -1;
while(!inStack.empty()){
outStack.push(inStack.top());
inStack.pop();
}
int value= outStack.top();
outStack.pop();
return value;
}
};
包含min函数的栈,同样需要两个栈,A、B为空时,第一个元素同时入A,B栈,对于后面的元素,若必此时B栈顶元素小,则入A,B栈,否则只入A栈,保持B中元素栈顶是最小的,此时,若输出栈中最小元素,则只需要输出B栈顶元素。(A是一个完整的栈,B是用来记录栈中最小的元素的。
class MinStack {
stack<int> Astack, Bstack;
public:
/** initialize your data structure here. */
MinStack() {
}
void push(int x) {
Astack.push(x);
if(Bstack.empty()||Bstack.top()>=x) Bstack.push(x);
}
void pop() {
int temp=Astack.top();//需先将A栈pop,比较B栈顶元素与此相等时,Bpop
Astack.pop();
if(temp==Bstack.top()){
Bstack.pop();
}
}
int top() {
return Astack.top();
}
int min() {
return Bstack.top();
}
};
day2
反转链表、从尾到头打印链表、复制复杂链表
反转链表408考研必须掌握的,因此比较熟悉。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *q=NULL;
ListNode *p=head;
ListNode *r;
while(p){
r=p->next;
p->next=q;
q=p;
p=r;
}
return q;//此时q指向新的表头,即原来的表尾
}
};
对于从尾到头打印链表,本题要返回一个vector,因此可以先逆置再添加到vector也可以利用栈先将链表元素入栈再添加到vector。
同样先将vector记录一下
#include <vector>
vector<int> array; //创建vector对象
array.push_back(a); //尾部插入数字a
array.pop_back();//删除向量最后一个元素
array.at(i); //使用下标访问元素
vector<int>::iterator it;//使用迭代器访问元素
for(it=array.begin();it!=array.end();i++)
cout<<*it<<endl;
array.insert(array.begin()+i,a);//在第i+1个元素前面插入a;
array.erase();//删除元素
array.erase(a,b);//删除区间
array.size();//向量大小
array.clear()//向量清空
array.empty()//判空
array.back();//最后一个元素
array.front();//第一个元素
这里使用栈
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
stack<int> s;
vector<int> res;
ListNode *p=head;
while(p){
s.push(p->val);//入栈
p=p->next;
}
while(!s.empty()){//栈非空时加到vector后面
res.push_back(s.top());
s.pop();
}
return res;
}
};
对于复制复杂链表,这是我第一次正式使用哈希表,虽然408学了原理,但实际也没怎么用过。本题使用unordered_map构建原结点和新结点的映射关系,因为存在了random这个指针,所以只能先把每个结点位置确定好后,再来连线。不能像构建普通链表那样一边创造新结点一边就连线。
class Solution {
public:
Node* copyRandomList(Node* head) {
if(head==nullptr) return nullptr;//若头结点为空则返回空
Node *cur =head; //cur指向原链表表头
unordered_map<Node*, Node*> map;//定义一个map用来存原结点和新结点的映射
while(cur!=nullptr){
map[cur]=new Node(cur->val);//构建哈希表每个cur在map里对应一个新结点
cur=cur->next;
}
cur = head;
while(cur!=nullptr){//连线
map[cur]->next = map[cur->next];
map[cur]->random = map[cur->random];
cur=cur->next;
}
return map[head];
}
};
哈希表确实很方便,学完组成原理对地址等知识有了理解后学习C之类的语言确实不会太一知半解了。
day3 字符串替换空格,字符串左旋转
C中常用的看的下面这个博客。
C++中字符串类常用操作
C++中字符串是可变长的,因此对于修改字符串可以在原字符串上进行,而java,python属于不可变长的,只能创造一个新的字符串。本题参考了C++的解答,也是很巧妙。
通过统计空格的个数,增加字符串的大小,再从字符串后往前修改。
class Solution {
public:
string replaceSpace(string s) {
int count=0, len=s.size();
for(char c:s){
if(c==' ') count++; //统计空格的数量
}
s.resize(len+2*count); //每个空格带来2个容量的扩充
for(int i=len-1,j=s.size()-1;i<j;i--,j--){
if(s[i]!=' ')
s[j]=s[i];
else{
s[j-2]='%';
s[j-1]='2';
s[j]='0';
j-=2;
}
}
return s;
}
};
字符串左旋转属于是408基础中的基础了,因此可以自己写出,时间复杂度高,空间复杂度低。
class Solution {
public:
void Reverse(string &s,int low,int high){
int i=low,j=high-1;
while(i<j){
char temp=s[i];
s[i]=s[j];
s[j]=temp;
i++;
j--;
}
}
string reverseLeftWords(string s, int n) {
int i=0,j=n,k=s.size();
Reverse(s,i,j);
Reverse(s,j,k);
Reverse(s,i,k);
return s;
}
};
day4 数组中重复的数字、排序数组中重复的数字、0~n-1中缺失的数字
对于重复的数字这题,也算是408数据结构经典题了,数字比数组长度小,只需重设一个数组计数即可,如下
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
int j=0;
int n= nums.size();
vector<int> count(n,0);
for(int i=0;i<n;i++){
count[nums[i]]++;
}
for(j;j<n;j++){
if(count[j]>1)
break;
}
return j;
if(j>=n) return -1;
}
};
我也想了用哈希表,但奈何对哈希不熟,看了题解后发现的确可以而且更简单,如下
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
unordered_map<int, bool> map;
for(int num:nums){
if(map[num]) return num; //若已出现过,直接返回此时的num
map[num]=true;
}
return -1;
}
};
升序数组中查找目标出现的次数,最易想到的就是从头到尾遍历,如下
class Solution {
public:
int search(vector<int>& nums, int target) {
int count = 0;
for(int i=0;i<nums.size();i++){
if(nums[i]==target)
count++;
if(nums[i]>target)
break;
}
return count;
}
};
但这样浪费了升序的条件,而升序适合使用二分查找,本题可通过二分查找找到数组中等于target的下边界和上边界,如下
class Solution {
public:
int search(vector<int>& nums, int target) {
int i=0,j=nums.size()-1;
while(i<=j){ //寻找有边界
int m=i+(j-i)/2;
if(nums[m]<=target)
i=m+1;
else
j=m-1;
}
int right=j;
i=0,j=nums.size()-1;
while(i<=j){ //寻找左边界
int m=i+(j-i)/2;
if(nums[m]>=target)
j=m-1;
else
i=m+1;
}
int left=i;
return right-left+1;
}
};
第三道同样是升序数组的查找问题,应直接想二分法,而我依然首先选择了遍历,
class Solution {
public:
int missingNumber(vector<int>& nums) {
int k=0;
int n=nums.size()+1;
vector<int> count(n,0);
for(int i=0;i<nums.size();i++){
count[nums[i]]++;
}
for(k;k<n;k++){
if(count[k]==0)
break;
}
return k;
}
};
毫无疑问时间复杂度和空间复杂度都很高。
其实就是找到第一个下标与数组内容不同的那个下标,因此可通过二分法将其分为两个数组。代码如下。
class Solution {
public:
int missingNumber(vector<int>& nums) {
int i=0, j=nums.size()-1;
while(i<=j){
int m = i+(j-i)/2;
if(m==nums[m]) i=m+1;
else j=m-1;
}
return i;
}
};
day5
二维数组中的查找、旋转数组的最小数字、第一个只出现一次的字符
二维数组的查找同样是行列升序的数组,想到二分法,每行都使用一次二分,时间复杂度 O ( n l o g 2 m ) O(nlog_2m) O(nlog2m), 我的代码如下
class Solution {
public:
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
if(matrix.size()==0)
return false;
int n=matrix.size();
int m=matrix[0].size();
for(int i=0;i<n;i++){
int low=0,high=m-1;
while(low<=high){
int mid= low+(high-low)/2;
if(matrix[i][mid]==target)
return true;
else if( matrix[i][mid]<=target)
low=mid+1;
else
high=mid-1;
}
}
return false;
}
};
跟官方题解比起来很冗长,他似乎用了很多C++内置的一些特殊用法,我这其实算是C语言hh。另外一种方法似乎是可以,从左下角往右上角遍历,若此时的数大于target,则令行-1,若小于target则令列+1。时间复杂度 O ( m + n ) O(m+n) O(m+n),代码如下:
class Solution {
public:
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
int i=matrix.size()-1; //行数
int j=0; //列数
while(i>=0&&j<matrix[0].size()){
if(matrix[i][j]>target)
i--;
else if(matrix[i][j]<target)
j++;
else
return true;
}
return false;
}
};
第二题旋转数组的最小数字,本题其实跟之前有一题很像,主要就是分成两个数组,找到右数组的起始,用二分法,但本题其实还是比较麻烦,因为当mid的值与端点值相同时,无法确定最小的数在左边还是右边,这也是我一直出错的地方,考虑的不太周到,看了题解发现,当无法判断时,只能缩小一点范围,也就是令j=j-1,重新判断。完整代码如下。
class Solution {
public:
int minArray(vector<int>& numbers) {
int i=0,j=numbers.size()-1;
while(i<j){
int mid=i+(j-i)/2;
if(numbers[mid]>numbers[j])
i=mid+1;
else if(numbers[mid]<numbers[j])
j=mid;
else j--;
}
return numbers[i];
}
};
第三题显然使用哈希,但还是不熟悉,看了题解才知道可以使用bool来表示
class Solution {
public:
char firstUniqChar(string s) {
unordered_map<char, bool> map;
for(char c:s)
map[c] = map.find(c)==map.end(); //这里好像是一个固定用法,但对find和end函数都不熟悉
for(char c:s)
if(map[c]) return c;
return ' ';
}
};
day6 按层的顺序打印二叉树Ⅰ、Ⅱ、Ⅲ
第一题按行顺序打印结点数字,就是二叉树的层序遍历,用C++写很多东西现成的,按照考研备考时的思路成功完成,看来层序遍历已经拿下,就是用一个队列来辅助,代码如下。
class Solution {
public:
vector<int> levelOrder(TreeNode* root) {
vector<int> res; //返回数组
queue<TreeNode*> q; //队列
TreeNode* p; //储存每次弹出的结点
if(root)
q.push(root); //根非空时,将根结点入队
while(q.empty()==false){ //队列不空时执行循环
p=q.front(); //将队首结点出队记作p
res.push_back(p->val); //将p的val写入res数组
if(p->left) //左孩子不空则左孩子入队
q.push(p->left);
if(p->right) //右孩子不空则右孩子入队
q.push(p->right);
q.pop(); //弹出队首结点
}
return res;
}
};
第二题需要返回一个二维数组,每一行是每层的元素,一直有个疑惑到底怎么样才能知道每一层的个数,题解巧妙使用了队列长度进行for循环,妙极了。代码如下。
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
queue<TreeNode*> q;
if(root)
q.push(root);
while(q.empty()==false){
vector<int> temp; //临时的vector用来存储每一行上的数
for(int i=q.size();i>0;i--){ //每一次循环结束后就将下一层的所有结点都入队了,
//此时通过while再次进入循环,得到的q.size就是这一层元素个数
TreeNode* p=q.front();
temp.push_back(p->val);
if(p->left) //剩下的步骤与题Ⅰ类似
q.push(p->left);
if(p->right)
q.push(p->right);
q.pop();
}
res.push_back(temp);
}
return res;
}
};
第三题z字形输出每一行的元素,用了个CPP里面的reverse函数讨巧办法先通过了,但明显本意不是如此,再学习一下大佬的思路。
day7 树的子结构、二叉树的镜像、对称的二叉树
树这里的算法题大概多用递归,本题判断B是否是A的子结构,成立的条件只有三种,一种是A、B的根节点相同,从而B是A的子结构,另外B可能隐藏在A的左子树或右子树里,因此可以递归的查找A、A的左子树、A的右子树。代码如下
class Solution {
public:
bool isSubStructure(TreeNode* A, TreeNode* B) {
if(A==nullptr||B==nullptr)
return false;
return (recur(A,B)||isSubStructure(A->left,B)||isSubStructure(A->right,B));//真的三种条件,满足其一即可
}
private:
bool recur(TreeNode* A, TreeNode* B){ //用来判断A和B根节点相同情况下,B是否包含在A中
if(B==nullptr) return true; //如果B是空指针,则说明B已经遍历完,返回真
if(A==nullptr||A->val!=B->val) return false; //若B非空的情况下A空了,或者AB的值不等则false
return (recur(A->left,B->left)&&recur(A->right,B->right)); //再对左右结点进行判断
}
};
二叉树的镜像、同样是递归,我想自上而下递归,大佬的题解似乎是自下而上,理解的不太好,还是写了自上而下的。
class Solution {
public:
TreeNode* mirrorTree(TreeNode* root) {
exchangeLR(root);
return root;
}
private:
void exchangeLR(TreeNode* root){
if(root==nullptr)
return;
swap(root->left,root->right); //交换左右结点
exchangeLR(root->left); //递归左子树
exchangeLR(root->right); //递归右子树
}
};
第三题一开始准备先求出镜像再比较是否相同的,但判断条件始终写不对,只好看了题解,只需要对左右子树进行判断是否相等即可。代码如下。
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if(root == NULL) return true;
return reer(root->left, root->right);
}
bool reer(TreeNode* A,TreeNode* B)
{
if(A == NULL && B==NULL) return true;
if(A == NULL || B==NULL || A->val != B->val) return false;
return reer(A->right, B->left)&&reer(A->left,B->right);
}
};
递归出口的理解一直不深刻导致递归一看就会一写就寄。只能多学习吧。