自己刷题时的代码,一般会尝试多种解法,都是AC的,时间超时的解法保留了,但是会注明;给大家刷题做一个参考;
基于leetcode平台,但是建议搭配着用牛客,leetcode有些题目改变了原书中的题意
后序发现错误会再更新,如果大家有发现错误,欢迎指正讨论啊;刷题的快乐是真的快乐,搞懂一个算法或者想到不一样的思路,想建立更好的反馈,还是多想多写多看多交流

一. 编程语言


#1 拷贝赋值运算符:

  • 返回值:自身引用;只有返回引用才可以连续赋值
  • 传入参数:声明为常量引用;传入实例则额外调用一次拷贝构造函数
  • 判断是否是同一个实例,否则继续拷贝;
  • 注意先释放了自身内存;先判断不是同一个实例才能放心释放
MyClass& Myclass::operator=(const MyClass& object) {if(this == &object) return *this;delete []m_pData; //释放之前的内存,和成员相关m_pData = nullptr;//把传入的类型的值拷贝过来m_pData = new char[strlen(object.m_pData)) + 1];strcpy(m_pData, object.m_pData);return *this;
}//如果内存不足,new申请新空间失败,则this已删除,作为nullptr暴露,不安全
更安全的方式,避免复制失败
MyClass& Myclass::operator=(const MyClass& object) {if(this != &object) {MyClass strTemp(object); //利用拷贝构造函数char* pTemp = strTemp.m_pData;strTemp.m_pData = this->m_pData;this->m_pData = pTemp;//交换内存,而不是直接销毁;退出该作用域后会自动调用析构函数}return *this;
}

拷贝构造函数:

  • 参数:const&;如果直接传值,则首先需要调用拷贝构造函数传值入函数,即我调用我自己(定义未完成,我想调用我自己但是不能调用)
  • 返回值:构造函数不需要返回值

移动构造函数

  • 参数:右值引用;移动后资源交给左侧对象操作,因此可以看作临时对象,声明为右值引用
  • 返回值:构造函数,不需要返回值

移动赋值运算符

  • 参数:同上,右值引用
  • 返回值:自身引用
  • 注意处理自赋值情况;检查后再删除原数据
  • 保证移动后的源对象是可解析的(不共享底层资源)

NOTE:移动右值,拷贝左值


#2 实现单例模式,singleton

确保一个类仅有一个实例;并提供全局访问

  • 构造函数保护:外部用户代码无权限创建对象,因此构造函数定义为private
  • 静态的类成员对象:私有化构造函数仅类内成员有权访问,唯一的实例化对象放在内,且定义为static
  • 唯一实例的全局访问:公共的getInstance()接口
  • 缺省状态下编译器会自动生成的,也需要进行保护,例如拷贝构造函数,operator=赋值运算符,析构函数等
1. 静态化不是实例模式 - 不完整的饿汉

所有成员变量和成员方法都用static修饰后,仍然不是实例模式
问题
1. 静态成员初始化顺序不依赖构造函数,无法保证
2. 静态成员不可能是virtual或是const,失去了重要的多态性

class Singleton
{
public:/* static method */
private:static Singleton _uniInstance; //static data member 在类中声明,在类外定义
};Singleton Singleton::uniInstance;
2. 饿汉模式

实例在程序运行时被立即初始化(空间换时间),线程安全

class Singleton {
public:static Singleton* getInstance() {return _uniInstance;}
private:Singleton(){};static Singleton* _uniInstance = new Singleton(); //static data member
}
Singleton* Singleton::_uniInstance = new Singleton();

问题:

  • 过早创建对象,内存效率被降低
  • 静态成员初始化顺序无法保证
    例如需要在ASingleton中使用BSingleton的实例,ASingleton可能先于BSingleton调用初始化构造函数,则ASingleton使用的是一个未初始化的内存区域
class ASingleton {
public:static ASingleton* getInstance() {return _uniInstance;}
private:ASingleton(){BSingleton::getInstance();};static ASingleton* _uniInstance;
}class BSingleton {
public:static BSingleton* getInstance() {return _uniInstance;}
private:BSingleton(){};static BSingleton* _uniInstance;
}Singleton* ASingleton::_uniInstance; //ASingleton早于BSingleton对象的实例化调用getInstance
Singleton* BSingleton::_uniInstance;
3. 懒汉模式-线程不安全

实例化只在第一次使用时初始化-时间换空间

class Singleton
{
public:static Singleton* getInstance(){if(!_uniInstance) uni_Instance = new Singleton();return _uniInstance;}
private:static Singleton* _uniInstance; Singleton(){}
};
Singleton* Singleton::_uniInstance = nullptr; //静态成员需要先初始化

问题:

  • 线程不安全:如果多个线程同步进入_uniInstance是否为空的判断(不一定同时,但此时无线程完成实例创建,因此所有线程判断都为真),则会分别进行实例创建,得到多个实例化对象
  • 析构函数的执行?
4.0 懒汉模式-线程安全

加锁(同步),一个线程加锁则其他线程只能等待

//直接加锁
Singleton* Singleton::getInstance() {Lock lock; //伪代码加锁if(!_uniInstance) _uniInstance = new Singleton();return _uniInstance;
}

问题:
效率低,加锁开销大

4.1 懒汉模式-双重锁
//双重校验锁,仅在第一次必要时使用锁
Singleton* Singleton::getInstance() {if(!_uniInstance) {Lock lock; //伪代码加锁if(!_uniInstance) _uniInstance = new Singleton();}return _uniInstance;
}

问题:内存读写的乱序执行
新建对象的步骤:

  1. 分配Singleton类型对象所需内存
  2. 在内存处构造类型的对象
  3. 把分配的内存的地址赋给指针
    实际执行中,1先执行,但是受编译器影响,2和3的顺序不一定;如果A线程执行时按1、3、2顺序执行,步骤3之后_uniInstance已经非空了,因此切换线程B会得到一个非空但未被构造的对象

解决方法:JAVA和c#中定义了volatile关键字,保证执行按照顺序完成,C++11之后可以跨平台实现类似功能

std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;/*
* std::atomic_thread_fence(std::memory_order_acquire);
* std::atomic_thread_fence(std::memory_order_release);
* 这两句话可以保证他们之间的语句不会发生乱序执行。
*/
Singleton* Singleton::getInstance() {Singleton* tmp = m_instance.load(std::memory_order_relaxed);std::atomic_thread_fence(std::memory_order_acquire);//获取内存fenceif (tmp == nullptr) {std::lock_guard<std::mutex> lock(m_mutex);tmp = m_instance.load(std::memory_order_relaxed);if (tmp == nullptr) {tmp = new Singleton;std::atomic_thread_fence(std::memory_order_release);//释放内存fencem_instance.store(tmp, std::memory_order_relaxed);}}return tmp;
}
//copyright https://segmentfault.com/a/1190000015950693
5. 懒汉模式改进

使用局部静态变量
局部静态变量仅初始化一次(C++11之后)

C++ memory model中对static local variable,说道The initialization of such a variable is defined to occur the first time control passes through its declaration; for multiple threads calling the function, this means there’s the potential for a race condition to define first.

class Singleton {
public:static Singleton& getInstance() {static Singleton _uniInsrance;return _uniInstance;}
private:Singleton(){};
}

NOTE:如果返回的是对局部静态对象指针,则调用者在getInstance函数结束后可能进行指针的销毁,因此无法进行实例化

编译器如何保证静态局部变量仅被初始化一次:很多编译器会通过全局的标志位记录该变量是否已经被初始化,这个过程类似懒汉模式的if条件;因此也可能存在线程安全问题(C++11之后还存在这个问题吗?)

bool flag = false;
if(!flag) {flag = true;staticVar = initStatic();
} //静态变量初始化的伪代码

问题:

  • 任意两个单例类的构造函数不能相互引用对方的实例,否则程序会崩溃
6. pthread_once()

linux中,pthread_once()可以保证某函数只被执行一次

class Singleton{
public:static Singleton* getInstance(){// init函数只会执行一次pthread_once(&ponce_, &Singleton::init);return m_instance;}
private:Singleton(); //私有构造函数,不允许使用者自己生成对象Singleton(const Singleton& other);//要写成静态方法的原因:类成员函数隐含传递this指针(第一个参数)static void init() {m_instance = new Singleton();}static pthread_once_t ponce_;static Singleton* m_instance; //静态成员变量
};
pthread_once_t Singleton::ponce_ = PTHREAD_ONCE_INIT;
Singleton* Singleton::m_instance=nullptr;
//copyright https://segmentfault.com/a/1190000015950693
Singleton的重用

用Singleton模板包装单例

//singleton模板类
template<typename T>
class Singleton
{
public:static T& getInstance() {static T value_; //静态局部变量return value_;}private:Singleton();~Singleton();Singleton(const Singleton&); //拷贝构造函数Singleton& operator=(const Singleton&); // =运算符重载
};

假设A、B两类均需要改造为单例

class A{
public:A(){a = 1;}void func(){cout << "A.a = " << a << endl;}private:int a;
};class B{
public:B(){b = 2;}void func(){cout << "B.b = " << b << endl;}
private:int b;
};int main()
{Singleton<A>::getInstance().func();Singleton<B>::getInstance().func();return 0;
}

也可以使用继承方式进行重用,单例模式需要重用的主要时getInstance()函数

class singletonInstance : public singleton<singletonInstance>...

NOTE:针对单件模式设计子类时,构造函数不能再定义为private,需要公开或protected,但是这样又不算真正的单件了,因为开放了实例化权力给别的类;如果一个单价被多次重用,那可能本身就不适合采用这个设计模式

NOTE:C++中应该减少类的相互依赖,可以通过设置全局静态遍历,并小心声明及使用顺序,来解决单例模式的问题;但是全局变量仍然无法完全保证实例的唯一性,也无法进行延迟实例化

二. 数据结构


#3 数组中重复的数字

数组长度n,数字的范围0~n-1。某个或多个数字重复一次或多次。找出任意一个重复的数字。

解法一:暴力法
  • 时间复杂度:最坏O(n^2)
  • 空间复杂度:最坏O(n)
解法二:哈希表 set/map记录cnt
class Solution {
public:int findRepeatNumber(vector<int>& nums) {set<int> s{};for(int i:nums){if(s.count(i) >= 1) return i;s.insert(i);}return -1;}
};
  • 时间复杂度:最坏O(n)
  • 空间复杂度:最坏O(n)
解法三:重排数组

使序号与值对应,即nums[0] = 0, nums[1] = 1

swap(num[i], nums[num[i]]) s.t. num[i] = i

0 1 2 3
2 3 1 0
1 3 2 0
3 1 2 0
0 1 2 3
class Solution {
public:int findRepeatNumber(vector<int>& nums) {if(nums.size() <2) return -1; //不合法输入for(int i=0; i<nums.size(); i++){if(nums[i]>nums.size()-1 || nums[i]<0) return -1; //不合法输入while(nums[i] != i){if(nums[i] == nums[nums[i]]) //已有数字归位,找到一个和自己相同的数return nums[i];elseswap(nums[i], nums[nums[i]]);}   }return -1;}
};
  • 时间复杂度:
    由于所有数字不一定按序,前期交换可能需要多次才能找到自己的位置的数,但是每次交换都一定使一个数字归位,因此时间效率O(n)
  • 空间复杂度:O(1) 也可以复制原数组后重排,避免修改元数据,空间复杂度O(n)
解法四:修改数组记录

同样思路,由第i位数字记录数字i的统计情况;但是不交换

class Solution {
public:int findRepeatNumber(vector<int>& nums) {int n = nums.size();if(n <= 1) return -1;for(int i = 0; i<n; ++i){int index = nums[i]%n;nums[index] += n;if(nums[index] >= 2*n) return index;}return -1;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
解法五:二分法查找

不修改数组,统计区间内数字个数是否超过应有个数,例如区间在0~3的数字超过4个,则重复数字一定在该区间内
但是本题条件下,个别情况不适用,例如 0 1 1 3 4 5 6 7

class Solution {
public:int findRepeatNumber(vector<int>& nums) {int n = nums.size();if(n <= 1) return -1;int l = 0, r = n-1;while(l <= r) {int mid = l + (r-l)/2;int cnt = count(nums, l, mid);if(l == r && cnt>1) return l;if(cnt > mid-l+1) r = mid;else l = mid+1;}return -1;}int count(vector<int>& nums, int l, int r) {if(l > r) return 0;int cnt{0};for(int i:nums){if(i >= l && i <= r) ++cnt;}return cnt;}
};
  • 时间复杂度:O(nlogn),调用O(logn)次,每次O(n)
  • 空间复杂度:O(1)
测试用例:
  • 长度n,包含重复数字,数字范围在0~n-1的数组
  • 数组不包含重复数字
  • 无效输入测试用例:输入空指针;长度为n,数字范围超过0~n-1

#4 二维数组的查找

数组从左到右递增,从上到下递增

解法一:暴力法

从头向尾遍历

class Solution {
public:bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {int m = matrix.size();if(m == 0) return false;int n = matrix[0].size();if(n == 0) return false;for(int i = 0; i<m; ++i){for(int j = 0; j<n; ++j)if(matrix[i][j] == target) return true;}return false;}
};
  • 时间复杂度:O(m*n)
  • 空间复杂度:O(1)
解法二:左下/右上,排除整行/整列

利用二维数组的递增特性

class Solution {
public:bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {int m = matrix.size();if(m == 0) return false;int n = matrix[0].size();if(n == 0) return false;int row{m-1}, col{0}; //左下while( row>=0 && col<n ){if(matrix[row][col] == target) return true;if(matrix[row][col] > target) row--;else col++;}return false;}
};
  • 时间复杂度:O(m+n)
  • 空间复杂度:O(1)
测试用例:
  • 二维数组包含target
  • 二维数组不包含target
  • 无效输入测试用例:输入空指针

#5 替换空格

把字符串中的每个空格替换为“%20”

解法一:字符数组

NOTE:空格占一个位置,替换后占3个字符位,要考虑搬移造成的开销
可以新建空间大小为3*n的数组,再切割返回
也可以先统计有多少个空字符,再在原字符数组尾部扩充,然后从后向前搬移(避免从前向后遍历的O(n^2)次的搬移)

class Solution {
public:string replaceSpace(string s) {if(s.empty()) return s;int cnt{0};for(auto p:s){if(p == ' ') ++cnt;}int p1 = s.size()-1;s += string(2*cnt, ' ');int p2 = s.size()-1;while(p1 >= 0 && cnt > 0){if(s[p1] != ' ') s[p2--] = s[p1];else{s[p2--] = '0';s[p2--] = '2';s[p2--] = '%';--cnt; }p1--;} return s;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
解法二:利用c++的str拼接
class Solution {
public:string replaceSpace(string s) {string res{};for(int i=0; i<s.size();i++){if(s[i] == ' ') res += "%20";else res += s[i];}return res;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例:
  • 包含空格字符
  • 无空格字符
  • 特殊输入:空字符;单个空格;连续空格
扩展题目:从尾向后的方法

两个排序数组A1、A2,将A2的数字插到A1中(A1空间足够)

双指针比较后,从后往前插入,避免重复覆盖


#6 从头到尾打印链表

解法一:数组反序

reverse函数

class Solution {
public:vector<int> reversePrint(ListNode* head) {if(head == nullptr) return {};ListNode* p = head;vector<int> res;while(p){res.push_back(p->val);p = p->next;}reverse(begin(res), end(res));return res;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
解法二:递归
class Solution {
public:vector<int> reversePrint(ListNode* head) {if(head == nullptr) return {}; //创建原始空数组vector<int> res = reversePrint(head->next);res.push_back(head->val);return res;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
解法三:事先遍历得到长度
class Solution {
public:vector<int> reversePrint(ListNode* head) {if(head == nullptr) return {};ListNode* p = head;int len(0);while(p){++len;p = p->next;}vector<int> res(len, -1);p = head;while(p){res[--len] = p->val;p = p->next;}return res;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
解法四:改变链表结构,反转链表后再打印
class Solution {
public:vector<int> reversePrint(ListNode* head) {if(head == nullptr) return {};ListNode* p = reverse(head);vector<int> res;while(p){res.push_back(p->val);p = p->next;}return res;}//递归反转链表ListNode* reverse(ListNode* head){if(head->next == nullptr) return head;ListNode* p = reverse(head->next);head->next->next = head;head->next = nullptr;return p;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例:
  • 多节点链表;单节点链表
  • 特殊输入:空指针

#7 重建二叉树

根据前序遍历和中序遍历结果,重建二叉树;返回头节点
前序:根->左->右
中序:左->右->根

解法一:递归

前序第一个数为根,根据这个数可以切割中序遍历结果为左子树及右子树两个区间

class Solution {
public:TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {if(preorder.empty() || inorder.empty()) return nullptr;TreeNode* cur = new TreeNode(preorder[0]);vector<int>::iterator iter = find(inorder.begin(), inorder.end(), preorder[0]);int leftLen = iter - inorder.begin();//左右子树的中序遍历数组vector<int> inleft(inorder.begin(), iter);vector<int> inright(iter+1, inorder.end());//左右子树的前序遍历数组vector<int> preleft(preorder.begin()+1, preorder.begin()+leftLen+1);vector<int> preright(preorder.begin()+leftLen+1, preorder.end());cur->left = buildTree(preleft, inleft);cur->right = buildTree(preright, inright);return cur; }
};
  • 时间复杂度:O(logn)
  • 空间复杂度:O(logn)
测试用例:

#8 二叉树的下一个节点

给定一棵二叉树和其中一个节点,找出中序遍历序列的下一个节点
node有指向父亲的指针

- 有右子树:下一个节点右子树的最左节点
- 无右子树:-自己是左子树:返回父亲-自己不是左子树:向上遍历,找到一个是左子树的节点,返回其父亲
  • 时间复杂度:O(logn)
  • 空间复杂度:O(1)
测试用例:
  • 普通二叉树
  • 特殊二叉树:全部没有左子节点;全部没有右子节点;单节点;空树
  • 不同位置的下一个节点

#9 用两个栈实现队列

实现push和pop

解法一:两个栈倒一倒
class CQueue {
public:CQueue() {}void appendTail(int value) {stack1.push(value);}int deleteHead() {if(stack2.empty()){while(!stack1.empty()){stack2.push(stack1.top());stack1.pop();}} //只有空了才从栈1取数据,注意要全部取空保证顺序int res{-1};if(!stack2.empty()){res = stack2.top();stack2.pop();} //取完仍然为空,则说明队列内无数据,返回-1return res;}
private:stack<int> stack1;stack<int> stack2;
};
  • 时间复杂度:插入:O(1);删除:最坏O(n),平均O(1)
  • 空间复杂度:O(n)
测试用例:
  • 向空队列中添加删除
  • 向非空队列添加删除
  • 连续删除元素直到队列为空
扩展题目:队列实现栈

实现push(), pop(), top(), empty()

解法一:push() O(n)
class MyStack {
public:/** Initialize your data structure here. */MyStack() {}/** Push element x onto stack. */void push(int x) {queue1.push(x);int loop = queue1.size()-1;while(loop--){int tmp = queue1.front(); queue1.pop();queue1.push(tmp);}}/** Removes the element on top of the stack and returns that element. */int pop() {int back = queue1.front();queue1.pop();return back;}/** Get the top element. */int top() {return queue1.front();}/** Returns whether the stack is empty. */bool empty() {return queue1.empty();}private:queue<int> queue1{};
};
解法二:top与pop O(n)
class MyStack {
public:/** Initialize your data structure here. */MyStack() {}/** Push element x onto stack. */void push(int x) {queue1.push(x);}/** Removes the element on top of the stack and returns that element. */int pop() {int back = fetch();swap(queue1, queue2);return back;}/** Get the top element. */int top() {int back = fetch();//交换,保证数据在queue1内,且按插入顺序swap(queue1, queue2);queue1.push(back);return back;}/** Returns whether the stack is empty. */bool empty() {return (queue2.empty() && queue1.empty());}//help function,把queue1的倒给queue2,留下最后一个数int fetch(){while(queue1.size()>1){queue2.push(queue1.front());queue1.pop();}int tmp = queue1.front(); queue1.pop();return tmp;}private:queue<int> queue1{};queue<int> queue2{};
};

三. 算法和数据操作


#10 斐波那契数列

解法一:递归

重复计算过多,超出时间限制

class Solution {
public:int fib(int n) {if(n < 0) return -1;if(n == 1) return 1;if(n == 0) return 0;return (fib(n-1) + fib(n-2));}
};
  • 时间复杂度:O(2^n),重复计算
  • 空间复杂度:O(n)
解法二:非递归
public:int fib(int n) {if(n < 0) return -1;if(n == 1) return 1;if(n == 0) return 0;int n0 = 0;int n1 = 1;for(int i = 2; i<=n; ++i){n1 += n0;n0 = n1 - n0;n1 %= 1000000007; //题目要求取余1000000007,中途直接操作,这样也不会造成溢出}return n1; //中途不取余,直接返回n1%1000000007,中途可能反生int溢出}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
解法三:数学方法
\begin{bmatrix}
{f(n)}&{f(n-1)}\\
{f(n-1)}&{f(n-2)}\\
\end{bmatrix} = \begin{bmatrix}
{1}&{1}\\
{1}&{0}\\
\end{bmatrix}^{n-1}
a^n = \begin{cases}
a^{n/2}*a^{n/2} \quad\quad\quad\quad\quad \ n为偶数 \\
a^{(n-1)/2}*a^{(n-1)/2}*a \quad n为奇数 \\
\end{cases}
  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(1)
测试用例:
  • 功能测试:3、5、7
  • 边界测试:0、1、2
  • 性能测试:45、50、100 较大数
题目扩展:青蛙跳台阶

青蛙一次只能跳1或2级台阶

class Solution {
public:int numWays(int n) {if(n == 0 || n == 1) return 1;int curN;int pre1 = 1;int pre2 = 1;for(int i = 2; i<=n; i++){curN = (pre1 + pre2)%1000000007;pre2 = pre1;pre1 = curN;}return curN;}
};

青蛙一次可以跳1~n个台阶 -> f(n) = 2^(n-1) (归纳法证明)


#11 旋转数组的最小数字

无重复数字

解法一:暴力法

依次遍历,没有利用旋转数组的特性(转折点)

class Solution {
public:int findMin(vector<int>& nums) {int len = nums.size();for(int i = 1; i<len; ++i) {if(nums[i] < nums[i-1]) return nums[i];}return nums[0];}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
解法二:二分法

右值针比前一个数大,左值针比前一个数小

class Solution {
public:int findMin(vector<int>& nums) {int left = 0;int right = nums.size() - 1;if(nums[left] <= nums[right]) return nums[0];while(left < right){int mid = left + (right-left)/2;//中点取左边界,如果条件判断后left=mid,可能会进入死循环(剩两个数时)if(nums[mid] > nums[right]) left = mid + 1;else right = mid; } return nums[left];}
};
  • 时间复杂度:O(logn)
  • 空间复杂度:O(1)
扩展题目

有重复数字:
有重复数字的时候,可能会出现左中右相等的情况,无法继续二分,只能按顺序遍历

class Solution {
public:int minArray(vector<int>& numbers) {if(numbers.size()<=0) return -1;int left = 0;int right = numbers.size()-1;if(numbers[left] < numbers[right]) return numbers[left]; //升序while(left < right){int mid = left + (right-left)/2;if((numbers[left] == numbers[right]) && (numbers[left] == numbers[mid])) {return inOrder(numbers, left, right);} //三个数相等时,利用help function顺序遍历if(numbers[mid] > numbers[right]) left = mid + 1;else if(numbers[mid] <= numbers[right] )right = mid;}return numbers[right];}int inOrder(vector<int> numbers, int left, int right){int res = numbers[left];for(int i = left+1; i <= right; i++){if(numbers[i] < res) res = numbers[i];}return res;}
};
  • 时间复杂度:O(logn) / O(n)
  • 空间复杂度:O(1)
测试用例:
  • 有重复数字数组,无重复数字数组
  • 升序数组,单个数字数组
  • 空数组

#12 矩阵中的路径

只能向上下左右移动一格,经过后的格子不能再次进入;判断矩阵中是否存在一条包含某字符串所有字符的路径

解法一:DFS遍历
class Solution {
public:int x[4] = {0, 0, -1, 1};int y[4] = {-1, 1, 0, 0}; //标记上下左右四个数的坐标int rows, cols;bool dfs(vector<vector<char>>& board, string& word, int i, int j, int pos){if(pos == word.size()) return true;char tmp = board[i][j];board[i][j] = '.'; //修改当前位的符号,防止路径再次重复进入for(int k=0; k<4; k++){int d_x = i + x[k];int d_y = j + y[k];if(d_x>=0 && d_x<rows && d_y>=0 && d_y<cols && board[d_x][d_y]==word[pos]){if(dfs(board, word, d_x, d_y, pos+1)) {return true;};}}board[i][j] = tmp; //失败,回退return false;}bool exist(vector<vector<char>>& board, string word) {if(board.size() <1 || board[0].size() <1 || word.size()<1) return false;rows = board.size();cols = board[0].size();for(int i=0; i<rows; i++){for(int j=0; j<cols; j++){if(board[i][j] == word[0]){if(dfs(board, word, i, j, 1)) return true;}}}return false;}
};
测试用例:
  • 功能测试:矩阵中存在/不存在路径
  • 边界测试: 一行/一列的矩阵;矩阵和路径所有字母相同
  • 特殊输入:空矩阵,空目标字符串

#12 机器人的运动范围

m行n列方格,允许向左右上下移动一格,但是不能进入行坐标和列坐标数位之和大于k的格子
e.g. (35,37)数位之和3+5+3+7=18

解法一:DFS,递归

从(0, 0)开始遍历,只需要向下和向右就可以遍历所有格子;
遍历过的格子进行标记;
如果一个格子不满足数位和小于k,则其右侧

class Solution {
public:int movingCount(int m, int n, int k) {if(k == 0) return 1;vector<vector<bool>> visited(m, vector<bool>(n, false));int count{0};movingCountCore(m, n, 0, 0, k, visited, count);return count;}void movingCountCore(int m, int n, int i, int j, int k, vector<vector<bool>>& visited, int& count){if(i<m && j<n && visited[i][j]==false && check(i, j, k)){ //从原点向右向下,下边界已肯定,只需要判断是否越上界count++;visited[i][j] = true;movingCountCore(m, n, i, j+1, k, visited, count); //向右movingCountCore(m, n, i+1, j, k, visited, count); //向下}}bool check(int i, int j, int k){return (i/10 + i%10 + j%10 + j/10) <= k;//已知m,n不大于100,坐标范围0~99;最多两位}
};
  • 时间复杂度:O(mn)
  • 空间复杂度:O(mn)
解法二:非递归 BFS

用queue记录节点,邻居节点入栈

class Solution {
public:int dx[2] = {0, 1};int dy[2] = {1, 0}; int movingCount(int m, int n, int k) {if(k == 0) return 1;vector<vector<bool>> visited(m, vector<bool>(n, false));int count{1};queue<pair<int, int>> q;q.push({0, 0});while(!q.empty()) {int x = q.front().first, y = q.front().second;q.pop();for(int i = 0; i<2; ++i) {int x_n = x + dx[i], y_n = y + dy[i];if(x_n>=m || y_n>=n || visited[x_n][y_n] || !check(x_n, y_n, k)) continue;q.push({x_n, y_n});++count;visited[x_n][y_n] = true;}}return count;}bool check(int i, int j, int k){return (i/10 + i%10 + j%10 + j/10) <= k;}
};
  • 时间复杂度:O(mn)
  • 空间复杂度:O(mn)
解法三:非递归遍历倒推

如果当前节点满足位数和小于k,且左和上的节点有一个可达,则其也可达

class Solution {
public:int movingCount(int m, int n, int k) {if(k == 0) return 1;vector<vector<int>> visited(m, vector<int>(n, 0));visited[0][0] = 1; //初始条件int count{1};for(int i = 0; i<m; ++i) {for(int j = 0; j<n; ++j) {if(visited[i][j] || !check(i, j, k)) continue;if(i-1>=0) visited[i][j] |= visited[i-1][j];if(j-1>=0) visited[i][j] |= visited[i][j-1];count += visited[i][j];}}return count;}bool check(int i, int j, int k){return (i/10 + i%10 + j%10 + j/10) <= k;}
};
  • 时间复杂度:O(mn)
  • 空间复杂度:O(mn)
测试用例:
  • 功能测试:多行多列,k值为正
  • 边界测试:单行单列,k为0
  • 特殊输入:k为负

#14 剪绳子

解法一:动态规划

对长度为i的绳子,在分割为长度为j和i-j的两段,返回最大值

product[i] = max(product[i], product[i-j]*product[j]) j\in[1, i/2]
class Solution {
public:int cuttingRope(int n) {if(n < 2) return 0;if(n == 2) return 1;if(n == 3) return 2;vector<int> product(n+1, 0);product[1] = 1;product[2] = 2;product[3] = 3;for(int i = 4; i<=n; i++){for(int j = 1; j<=i/2; j++){product[i] = max(product[i], product[j]*product[i-j]);}}return product[n];}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
解法二:贪婪算法
n>5时, 有(n-2)*2 > n  \quad \quad \quad\quad \quad  (n-3)*3 > n 

n = 1~4时,另外考虑(题目要求最少要切一次)
且3>1*2,优先拆3

class Solution {
public:int cuttingRope(int n) {if(n < 2) return 0;if(n == 2) return 1;if(n == 3) return 2;long res{1};if(n%3 == 1){ //余4,可以拆成两个2n-=4;res = 4; }if(n%3 == 2){ //余2,只能拆成一个2n-=2;res = 2;}while(n){res *= 3;res %= 1000000007; //题目要求取余n -=3;}return res;}
};
  • 时间复杂度:O(1)
  • 空间复杂度:O(1)
测试用例:
  • 功能测试:正常数
  • 性能测试:绳长很大
  • 边界测试:0,1,2,3,4

#15 二进制中1的个数

统计数字用二进制表示后,1的个数

解法一:n&(n-1)可以去掉末位1
class Solution {
public:int hammingWeight(uint32_t n) {int cnt{0};while(n){cnt ++;n = n&(n-1);}return cnt;}
};
  • 时间复杂度:O(1)
  • 空间复杂度:O(1)
解法二:正经移位

输入为unsigned int,可以左移;如果输入int,且int为负时,左移首位补1,会陷入死循环

class Solution {
public:int hammingWeight(uint32_t n) {int cnt{0};while(n){if(n&1) ++cnt; //cnt += n&1;n = n>>1; //移位的效率比除2要高//cnt += n%2;//n /= 2;}return cnt;}
};
  • 时间复杂度:O(1)
  • 空间复杂度:O(1)
解法三:反向移位

右移补零

class Solution {
public:int hammingWeight(uint32_t n) {int cnt{0};uint32_t flag{1};while(flag){if(n&flag) ++cnt;flag = flag<<1; }return cnt;}
};
  • 时间复杂度:O(1)
  • 空间复杂度:O(1)
测试用例:
  • 正数、负数
  • 边界数:1,0x7FFFFFFF,0x80000000,0xFFFFFFFF

四.代码的完整性

功能测试 + 边界测试 + 负面测试(特殊输入/错误输入)

#16 数值的整数次方

不使用库函数实现求平方pow

需要注意处理:

  • n为负数:x处理为1/x,n = -n
  • INT_MIN,无法转换成正数,需要最后再补乘一个x
解法一:暴力法

超时;

class Solution {
public:double myPow(double x, int n) {if(x == 0 || x == 1 || n == 1) return x;int flag_min{0};if(n < 0) {x = 1/x;if(n == INT_MIN) {n = INT_MAX;flag_min = 1; //也可以将n转换为long再存储}else n = -n;}double res{1};while(n--) res *= x;if(flag_min) res *= x;return res;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
解法二:二分乘方
a^n = \begin{cases}
a^{n/2}*a^{n/2} \quad\quad\quad\quad\quad \ n为偶数 \\
a^{(n-1)/2}*a^{(n-1)/2}*a \quad n为奇数 \\
\end{cases}
class Solution {
public:double myPow(double x, int n) {if(n == 0 || x == 1) return 1;if(x == 0 || n == 1) return x;int flag_min{0};if(n < 0) {x = 1/x;if(n == INT_MIN) {n = INT_MAX;flag_min = 1;}else n = -n;}double res = myPow(x, n>>1);res *= res;if(n&0x1) res *= x;if(flag_min) res *= x;return res;}
};
  • 时间复杂度:O(logn)
  • 空间复杂度:O(1)
测试用例:
  • 功能测试:正常n,正常x,负n正n,负x正x
  • 边界测试:x、n值为0、1时

#17 打印从1到最大的n位数

leetcode要求输出vector数组,且测试例中不会越界,与书中要求不符;因此按照书中要求直接输出

解法一:暴力法

n较大时溢出

void printNumbers(int n) {int max = pow(10, n);int pos = 1;while(pos < max) {cout << pos << " ";++pos;}
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
解法二:用字符串/数组表示数字
void printNumbers(int n) {if(n<=0) return;vector<int> num(n, 0);for(int i = 0; i<10; ++i) {num[0] = 0;printNumbers(num, n, 1);}
}void printNumbers(vector<int>& num, int n, int index) {if(index == n) {print(num);return;}for(int i = 0; i<10; ++i) {num[index] = i;printNumbers(num, n, index+1);}
}void print(vector<int>& num) {bool isBegin = true;int n = num.size();for(auto i:num) {if(isBegin && i == 0) continue;if(isBegin) isBegin = false;cout << i;}cout << " ";
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
测试用例:
  • 功能测试:正常大小的n
  • 性能测试:n很大
  • 边界测试:n=0

#18 删除链表节点

给定头指针和一个节点的指针,在O(1)时间内删除该节点

解法一:覆盖

仍然需要处理,待删节点的下一个是空指针的情况,以及只有一个节点的情况

ListNode* deleteNode(ListNode* head, ListNode* toBeDelete) {if(head == nullptr || toBeDelete == nullptr) return nullptr;if(toBeDelete->next != nullptr) {ListNode* pNext = toBeDelete->next;toBeDelete->val = pNext->val;toBeDelete->next = pNext->next;//leetcode中,回收操作通常由析构函数自动进行delete pNext;pNext = nullptr;}//下一个为空指针,且链表一共一个节点else if(head == toBeDelete) {delete head; //delete toBeDelete; //head和toBeDelete指向同一个地址,释放一遍内存head = toBeDelete = nullptr; //两个指针变量,全部需要收纳}//删除尾节点,需要从头开始遍历else {ListNode* p = head;while(p->next != toBedelete) p = p->next;p->next = nullptr;delete toBeDelete;toBeDelete = nullptr;}
}
  • 时间复杂度:平均O(1),删除尾节点时最差O(n)
  • 空间复杂度:O(1)
测试用例:
  • 功能测试:正常的链表中间的节点指针
  • 边界测试:删除头节点;删除尾节点;仅有一个节点;
  • 异常输入:删除的节点不在链表内
扩展题目 删除链表中重复的节点

删除排序数组中的重复节点,例如 1->2->3->3->4变为1->2->4
头节点也有可能重复从而被删除,因此需要设置pre指针

ListNode* DeleteDuplica(ListNode* head) {ListNode* preHead = new ListNode(-1);preHead->next = head;ListNode* preNode = preHead;ListNode* curNode = head;while(curNode && curNode->next) {if(curNode->val != curNode->next->val) {preNode = curNode;curNode = curNode->next;}else {int target = curNode->val;while(curNode && curNode->val == target) {ListNode* nextNode = curNode->next;preNode->next = nextNode;//在leetcode平台测试,直接省略了delete toBeDeletecurNode = nextNode;}}}return preHead->next;
}
测试用例:
  • 功能测试:重复节点位于开始、中间、结尾;不含重复节点
  • 特殊输入:空链表;所有节点均为重复

#19 正则表达式匹配

规则:’.‘可以匹配任意一个字符,而’*'表示它前面的字符可以出现任意次(含0次)

解法一:动态规划

NOTE:dp[i][j]表示两个字符串的前i和前j个字符能否匹配,所以实际比较的下标位数为i-1和j-1

初始化规则:初始化第一行,即第一个字符串为空

  • 匹配字符串也为空时,直接匹配:dp[0][0] = true
  • 非空的正则表达式匹配空字符串时:dp[0][j] = true当且仅当匹配字符串形如a*b*c*...z* (’.'不能匹配0个字符)

匹配规则

  • s[i-1] == p[j-1]或者p[j-1] == '.',当前位匹配,取决于前一位结果:dp[i][j] = dp[i-1][j-1]
  • 否则:仅当p[j-1] == '*',才有可能匹配
    • 如果p的前一位和当前位是匹配的(p[j-1] == s[i-1] || p[j-1] == '.'),因为此时’*'前面这位可以取1次或多次,所以s的当前位可以直接忽略,即dp[i][j] = dp[i-1][j]
    • 而不管是否匹配,’*'都可以选择匹配0次,即dp[i][j] = dp[i][j-2]
class Solution {
public: bool isMatch(string s, string p) {if(!s.empty() && p.empty()) return false;int rows = s.length();int columns = p.length();vector<vector<bool>> dp(rows+1, vector<bool>(columns+1, false));dp[0][0] = true;for(int j=1;j<=columns;j++) {   if(p[j-1] == '*' && dp[0][j-2]) dp[0][j] = true;}for(int i=1; i<=rows; ++i) {for(int j=1; j<=columns; ++j) {char nows = s[i-1];char nowp = p[j-1];if(nows==nowp || nowp == '.') dp[i][j] = dp[i-1][j-1];else if(nowp=='*') {if(j>=2){char nowpLast = p[j-2];if( nowpLast == nows || nowpLast=='.') dp[i][j] = dp[i-1][j];dp[i][j] = dp[i][j]||dp[i][j-2];}}}}return dp[rows][columns];}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
解法二:递归

超时

class Solution {
public:bool isMatch(string s, string p) {if(p.empty() && s.empty()) return true;if(p.empty()) return false;return isMatchCore(s, p, 0, 0);}bool isMatchCore(string& s, string& p, int s_pos, int p_pos){if(s_pos == s.size() && p_pos == p.size()) return true;if(s_pos != s.size() && p_pos == p.size()) return false;if(s_pos > s.size()) return false; if(p[p_pos] == s[s_pos] || (p[p_pos] == '.' && s_pos != s.size()))return isMatchCore(s, p, s_pos+1, p_pos+1);if(p[p_pos+1] == '*'){if(p[p_pos] == s[s_pos] || (p[p_pos] == '.')){return( isMatchCore(s, p, s_pos, p_pos+2) ||isMatchCore(s, p, s_pos+1, p_pos) ||isMatchCore(s, p, s_pos+1, p_pos+2)); //取0次多次1次}elsereturn isMatchCore(s, p, s_pos, p_pos+2); //取0次}return false;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
测试用例:
  • 功能测试:包含普通字符,’.’,’*’;模式字符串和输入字符串匹配/不匹配
  • 特殊输入:空字符

#20 表示数值的字符串

判断字符串是否表示字符,包括正数、复数、科学计数法表示

难点在于leetcode并没有将规则说清楚,需要一次一次提交试错判断规则

class Solution {
public:bool isNumber(string s) {while(s[0] == ' ') s = s.substr(1);while(s[s.size()-1] == ' ') s = s.substr(0, s.size()-1);if(s[0] == '+' || s[0] == '-') s = s.substr(1);if(s.empty()) return false;int count_E{0}, pos_E{-1};for(int i = 0; i<s.size(); i++){if(s[i] == 'e') { //题意规定,科学计数法仅小写e合法,大写E不认为是数值count_E++;pos_E = i;}}if(count_E > 1) return false;if(pos_E != -1){ //科学计数法需要分为两半进行判断string base = s.substr(0, pos_E), pow = s.substr(pos_E+1);return isValidNum(base, false) && isValidNum(pow, true);}return isValidNum(s, false); //不含e}bool isValidNum(string &s, bool isTail){if(s.empty()) return false;if(isTail && (s[0] == '+' || s[0] == '-')) s = s.substr(1); //指数部分可以含+/-号int countNum = 0, countDot = 0;while( !s.empty() && ((s[0] >= '0' && s[0] <= '9' )|| s[0] == '.')){if(s[0] == '.') countDot++;else countNum++;s = s.substr(1);} //isTail表示为指数部分,要求不能为空也不能为小数if(isTail) return s.empty() && (countNum > 0) && (countDot == 0);//非指数部分则可以含小数点return s.empty() && (countNum > 0) && (countDot < 2);//题目要求中,形如0.e5、0e5、0e0都是合法的}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例:
  • 功能测试:正数、负数,各种形式的字符串;包含非法字符的字符串…
  • 特殊输入:空字符串

#21 调整数组顺序使奇数位于偶数前面

洗牌后奇数在输组前半部分,偶数在数组后半部分
不要求保持奇数或偶数组的内部相对顺序

解法一:暴力法

从头向尾遍历,遇到偶数则取出,将后面的所有数向前挪一位,再将该数插到尾后;不能保证顺序

class Solution {
public:vector<int> exchange(vector<int>& nums) {if(nums.empty()) return nums;int end = nums.size();int pos = 0;while(pos < end) {while(nums[pos]&1) ++pos;if(pos >= end) break;move(nums, pos, end);--end; //要记住之前已经移动的偶数数量,否则所有偶数都在末端后会无限循环}return nums;}void move(vector<int>& nums, int i, int end) {int target = nums[i];for(int x = i+1; x<end; ++x) {nums[x-1] = nums[x];}nums[end-1] = target;}
};
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)
解法二:双指针

一个指针指向奇数,从前向后,一个指针指向偶数,从后向前

class Solution {
public:vector<int> exchange(vector<int>& nums) {if(nums.empty()) return nums;int odd = 0;int even = nums.size()-1;while(true){              //用函数来判断数字性质,可移植性更强while(odd <= even && isOdd(nums[odd]) odd++; //遇到奇数跳过while(odd <= even && isEven(nums[even]) even--; //遇到偶数跳过 if(odd > even) return nums;swap(nums[odd], nums[even]);}return nums;}bool isOdd(int i) {return i&1;}bool isEven(int i) {return !(i&1);}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
解法三 暴力法:额外空间

遇到奇数从前向后填充,遇到偶数从后向前填充

class Solution {
public:vector<int> exchange(vector<int>& nums) {if(nums.empty()) return nums;int len = nums.size();int odd = 0, even = len-1;vector<int> res(len, 0);for(int i = 0; i<len; ++i) {if(nums[i]&1) res[odd++] = nums[i];else res[even--] = nums[i]; }return res;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
测试用例:
  • 功能测试:奇数偶数随机交替;奇数全部在前半;偶数全部在前半
  • 特殊输入:单个数字数组;空数组

五. 代码的鲁棒性


#22 链表中倒数第k个节点

解法一:暴力法

统计链表长度,再按照n-k从头向后遍历,切割

class Solution {
public:ListNode* getKthFromEnd(ListNode* head, int k) {if(head == nullptr) return nullptr;int cnt{0};ListNode* ptr = head;while(ptr != nullptr) {++cnt;ptr = ptr->next;}if(k > cnt) return nullptr;cnt -= k;ptr = head;while(cnt--) {ptr = ptr->next;}return ptr;}
};
  • 时间复杂度:O(n + n-k)
  • 空间复杂度:O(1)
解法二:快慢指针

快指针先走k步,慢指针再开始走;等快指针走到链表尾段时,慢指针指向倒数第k个

class Solution {
public:ListNode* getKthFromEnd(ListNode* head, int k) {if(head == nullptr) return nullptr;ListNode* slow{head};ListNode* fast{head};int ahead = k-1;while(ahead--){fast = fast->next;if( fast == nullptr ) return nullptr;}while(fast->next != nullptr){fast = fast->next;slow = slow->next;}return slow;}
};
  • 时间复杂度:O(n + k)
  • 空间复杂度:O(1)
测试用例:
  • 功能测试:正常链表,n>k
  • 性能测试:长度很长的链表
  • 特殊输入:空指针;n<k;n=k;

#23 链表环的入口节点

解法一:快慢指针

快指针和慢指针,相遇位置一定在环内
假设单链部分长度A,环长度B,相遇位置距入口b,则有 A+b+kB = 2(A+b) 有A = kB-b
快指针从当前位置,另一个指针从头开始,相同步幅,再相遇一定在入口处;(快指针此时在环内位置为b,再走A,有b+A = kB,正好在环的起始位置)

ListNode* entryNodeOfLoop(ListNode* head) {if(head == nullptr) return nullptr;ListNode* slow = head;ListNode* fast = head;while(fast != slow) {if(fast == nullptr || fast->next == nullptr) return nullptr;fast = fast->next->next;slow = slow->next;}slow = head;while(slow != fast) {slow = slow->next;fast = fast->next;}return fast;
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
解法二:快慢指针
  • 进入环内,计数环内节点数n
  • 快指针早走n步,则快慢指针一定在入口处相遇
ListNode* entryNodeOfLoop(ListNode* head) {if(head == nullptr) return nullptr;int n = countLoop(head);ListNode* fast = head;ListNode* slow = head;while(n--) fast = fast->next;while(slow != fast) {slow = slow->next;fast = fast->next;}return fast;
}int countLoop(ListNode* head) {if(head == nullptr) return 0;ListNode* slow = head;ListNode* fast = head;while(fast != slow) {if(fast == nullptr || fast->next == nullptr) return 0;fast = fast->next->next;slow = slow->next;}int cnt{1};fast = fast->next;while(fast != slow) {++cnt;fast = fast->next;}return cnt;
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例
  • 功能测试:不含环,含环,环中有多个或单个节点
  • 特殊输入:空节点

#24 反转链表

解法一:迭代
class Solution {
public:ListNode* reverseList(ListNode* head) {if(head == nullptr || head->next == nullptr) return head;ListNode* first = reverseList(head->next);head->next->next = head;head->next = nullptr;return first;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n) 递归调用
解法二:递归
class Solution {
public:ListNode* reverseList(ListNode* head) {ListNode* prev{};while(head !=  nullptr){ListNode* tmp{head->next};head->next = prev;prev = head;head = tmp;}return prev;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例:
  • 功能测试:一般数组
  • 特殊输入:空指针

#25 合并两个有序数组

解法一:双指针
class Solution {
public:ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {ListNode* preHead = new ListNode(-1);ListNode* cur = preHead;while(l1 != nullptr && l2 != nullptr){if(l1->val < l2->val){cur->next = l1;cur = cur->next;l1 = l1->next;}else{cur->next = l2;cur = cur->next;l2 = l2->next;}}while(l1 != nullptr){cur->next = l1;cur = cur->next;l1 = l1->next;}while(l2 != nullptr){cur->next = l2;cur = cur->next;l2 = l2->next;}return preHead->next;}
};
  • 时间复杂度:O(m+n)
  • 空间复杂度:O(1)
解法二:递归
class Solution {
public:ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {if(l1 == nullptr) return l2;if(l2 == nullptr) return l1;ListNode* head;if(l1->val < l2->val) {head = l1;head->next = mergeTwoLists(l1->next, l2);}else {head = l2;head->next = mergeTwoLists(l1, l2->next);}return head;}
};
测试用例:
  • 功能测试:两个有序链表,等长或者不等长;一个链表数字完全在另一个链表之前;一个数字组成的两个链表
  • 特殊输入:一个链表为空;

#26 树的子结构

解法一:dfs
class Solution {
public:bool isSubStructure(TreeNode* A, TreeNode* B) {if (A==nullptr || B==nullptr) return false;return dfs(A, B) || isSubStructure(A->left, B) || isSubStructure(A->right, B);}bool dfs(TreeNode* A, TreeNode* B) {if (B==nullptr) return true; if (A==nullptr) return false;return Equal(A->val, B->val) && dfs(A->left, B->left) && dfs(A->right, B->right);}bool Equal(int num1, int num2){return abs(num1-num2) < 0.000000001; //如果val通过double类型存储,直接比较可能精度不够}
};
测试用例:
  • 功能测试:两课树,是/不是子结构
  • 特殊输入:空树

#27 二叉树镜像

解法一:迭代
class Solution {
public:TreeNode* mirrorTree(TreeNode* root) {stack<TreeNode*> s;s.push(root);while(!s.empty()){TreeNode* cur = s.top();s.pop();if(cur == nullptr) continue;swap(cur->left, cur->right);s.push(cur->left);s.push(cur->right);}return root;}
};
解法二:递归
class Solution {
public:TreeNode* mirrorTree(TreeNode* root) {if(root == nullptr || (root->left == nullptr && root->right == nullptr)) return root;swap(root->left, root->right);if(root->left) mirrorTree(root->left);if(root->right) mirrorTree(root->right);return root;}
};
测试用例:
  • 功能测试:普通二叉树;没有左子节点或右子节点的二叉树;只有一个节点的二叉树
  • 特殊输入:空二叉树

28 对称二叉树

方法一:递归
class Solution {
public:bool isSymmetric(TreeNode* root) {return isSymmetric(root, root);}bool isSymmetric(TreeNode* node1, TreeNode* node2){if(node1 == NULL && node2 == NULL) return true;if(node1 == NULL || node2 == NULL) return false;if(node1->val != node2->val) return false;return isSymmetric(node1->left, node2->right) && isSymmetric(node1->right, node2->left);}};
方法二:迭代
class Solution {
public:bool isSymmetric(TreeNode* root) {if(root == nullptr) return true;stack<TreeNode*> s1;stack<TreeNode*> s2;s1.push(root);s2.push(root);while(!s1.empty() && !s2.empty()) {TreeNode* t1 = s1.top(); s1.pop();TreeNode* t2 = s2.top(); s2.pop();if(t1 == nullptr && t2 == nullptr) continue;if(t1 ==  nullptr || t2 == nullptr || t1->val != t2->val) return false;s1.push(t1->left); s1.push(t1->right);s2.push(t2->right); s2.push(t2->left); }return s1.empty() && s2.empty();}
};
测试用例
  • 功能测试:对称的二叉树;非对称二叉树;结构对称但是值不对称

#29 顺时针打印矩阵

顺时针,每圈一个循环;注意判断行列数,是否需要进到后面三个循环中

class Solution {
public:int cols, rows;vector<int> spiralOrder(vector<vector<int>>& matrix) {vector<int> res{};if(matrix.empty() || matrix[0].empty()) return res;rows = matrix.size();cols = matrix[0].size();int iter = (min(rows, cols)-1)/2;for(int start = 0; start<=iter; start++){printSpiralOrder(matrix, start, res);}return res;}void printSpiralOrder(vector<vector<int>>& matrix, int start, vector<int>& res){int r_end = rows - start - 1;int c_end = cols - start - 1;for(int i = start; i <= c_end; i++){res.push_back(matrix[start][i]);} if(start < r_end){for(int i = start+1; i<=r_end; i++){res.push_back(matrix[i][c_end]);}}if(start < r_end && start < c_end){for(int i = c_end-1; i>=start; i--){res.push_back(matrix[r_end][i]);}}if(start < c_end && start < r_end-1){for(int i = r_end-1; i>start; i--){res.push_back(matrix[i][start]);}}}
};

#30 包含min的栈

使用辅助栈,辅助栈的元素数量和栈同步;如果当前数大于辅助栈顶数,则重新推入栈顶,否则推入当前数
出栈也同步弹出;这样操作保证辅助栈栈顶一直都是当前栈内元素的最小值;

class MinStack {
public:/** initialize your data structure here. */MinStack() {}void push(int x) {_data.push(x);if(_min.empty()) _min.push(x);else{if(x < _min.top()) _min.push(x);else _min.push(_min.top());}}void pop() {_data.pop();_min.pop();}int top() {return _data.top();}int min() {return _min.top();}
private:stack<int> _data{};stack<int> _min{};
};
测试用例:
  • 新压入栈的数比之前最小数大
  • 新压入栈的数比之前最小数小
  • 弹出非最小数
  • 弹出当前最小数

#31 栈的压入弹出序列

思路:通过模拟栈的压入弹出过程,验证是否为合法的序列;两个指针分别指向两个序列

解法一:以压入序列为轴

按顺序压栈,压栈后按出栈顺序弹出;顺利清空栈,则说明出栈序列是合法的

class Solution {
public:bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {int p_pop{0};stack<int> help{};for(int i = 0; i<pushed.size(); i++){help.push(pushed[i]);while(!help.empty() && help.top() == popped[p_pop]){p_pop++;help.pop();}}return help.empty();}
};
解法二:以弹出序列为轴

按照出栈顺序尝试压栈且弹出

class Solution {
public:bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {if(pushed.size() != popped.size()) return false;int p_pop{0}, p_push{0};int maxP = popped.size();stack<int> help{};while(p_pop < maxP) {while(p_push < maxP && ((!help.empty() && popped[p_pop] != help.top()) || help.empty())) {help.push(pushed[p_push]);++p_push;}if(p_push == maxP && (!help.empty() && popped[p_pop] != help.top())) return false;    while(!help.empty() && popped[p_pop] == help.top()) {help.pop();++p_pop;}}return true;}
};
测试用例:
  • 功能测试:多个数字及一个数字数组,合法/不合法弹出序列
  • 特殊输入:两个空数组;不等长数组

#32 从上到下打印二叉树

层序遍历

class Solution {
public:vector<int> levelOrder(TreeNode* root) {queue<TreeNode*> bfs{};vector<int> res{};if(!root) return res;bfs.push(root);while(!bfs.empty()){TreeNode* tmp = bfs.front();bfs.pop();if(tmp){res.push_back(tmp->val);bfs.push(tmp->left);bfs.push(tmp->right);} }return res;}
};
扩展:分行从上到下打印
class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {vector<vector<int>> printNodeTree{};if(!root) return printNodeTree;queue<TreeNode*> Nodes{};Nodes.push(root);while(!Nodes.empty()){vector<int> resTmp{};int loop = Nodes.size();while(loop){TreeNode* tmp = Nodes.front();Nodes.pop();if(tmp) resTmp.push_back(tmp->val);if(tmp->left) Nodes.push(tmp->left);if(tmp->right) Nodes.push(tmp->right);loop--;}if(!resTmp.empty()) printNodeTree.push_back(resTmp);}return printNodeTree;}
};
扩展:之字形从上到下打印
class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {vector<vector<int>> printNodeTree{};if(!root) return printNodeTree;deque<TreeNode*> Nodes{};Nodes.push_back(root);bool isBackwards{false};while(!Nodes.empty()){vector<int> resTmp{};int loop = Nodes.size();if(isBackwards) printNodeReverse(loop, Nodes, resTmp);else printNode(loop, Nodes, resTmp);if(!resTmp.empty()) printNodeTree.push_back(resTmp);isBackwards = !isBackwards;}return printNodeTree;}void printNode(int loop, deque<TreeNode*>& Nodes, vector<int>& res){while(loop && !Nodes.empty()){TreeNode* tmp = Nodes.front();Nodes.pop_front();if(tmp) res.push_back(tmp->val);if(tmp->left) Nodes.push_back(tmp->left);if(tmp->right) Nodes.push_back(tmp->right);loop--;}     }void printNodeReverse(int loop, deque<TreeNode*>& Nodes, vector<int>& res){while(loop && !Nodes.empty()){TreeNode* tmp = Nodes.back();Nodes.pop_back();if(tmp) res.push_back(tmp->val);if(tmp->right) Nodes.push_front(tmp->right);if(tmp->left) Nodes.push_front(tmp->left);loop--;}    }
};

或者用两个栈,实现顺序颠倒

class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {if(root == nullptr) return {};stack<TreeNode*> level[2];vector<vector<int>> res;bool flag = false;level[0].push(root);while(!level[0].empty() || !level[1].empty()) {int num = flag?level[1].size():level[0].size();vector<int> nodes;if(flag) {while(num--) {TreeNode* tmp = level[1].top(); level[1].pop();nodes.push_back(tmp->val);if(tmp->right) level[0].push(tmp->right);if(tmp->left) level[0].push(tmp->left);}}else {while(num--) {TreeNode* tmp = level[0].top(); level[0].pop();nodes.push_back(tmp->val);if(tmp->left) level[1].push(tmp->left);if(tmp->right) level[1].push(tmp->right);}                }res.push_back(nodes);flag = !flag;} return res;}
};

也可以最后将vector压入时,压入reverse处理过的vector,但是会牺牲一半的时间复杂度

测试用例
  • 功能测试:完全二叉树;仅有左/右子树的二叉树
  • 特殊输入:根节点为nullptr,单节点二叉树

#33 二叉搜索树的后序遍历序列

判断一个序列是否可能为二叉搜索树后序遍历序列
后序遍历序列的特点:

  • 最后一个数为根节点
  • 前面的数组可以被分为两个部分,前半部分均小于根节点;后半部分均大于根节点
    递归遍历每棵子树
class Solution {
public:bool verifyPostorder(vector<int>& postorder) {if(postorder.empty() || postorder.size() == 1) return true;return isPostorder(postorder, 0, postorder.size()-1);}bool isPostorder(vector<int>& postorder, int start, int end){if(end <= start) return true;int rootVal{postorder[end]};int pos{start};for(; pos<end; pos++){if(postorder[pos] > rootVal)break;}int j = pos+1;for(; j<end; j++){if(postorder[j] < rootVal)return false;}return isPostorder(postorder, start, pos-1) && isPostorder(postorder, pos, end-1);}
};

或者如下,不用helpfunction,但是需要额外的临时空间

class Solution {
public:bool verifyPostorder(vector<int>& postorder) {if(postorder.empty() || postorder.size() == 1) return true;int rootVal{postorder[postorder.size()-1]};int pos{0};for(; pos<postorder.size()-1; pos++){if(postorder[pos] > rootVal)break;}int j = pos;for(; j<postorder.size()-1; j++){if(postorder[j] < rootVal)return false;}vector<int> left{postorder.begin(), postorder.begin()+pos};vector<int> right{postorder.begin()+pos, postorder.begin()+postorder.size()-1};return verifyPostorder(left) && verifyPostorder(right);}
};
测试用例
  • 功能测试:各种造型的树的后序遍历序列;非合法后序遍历序列
  • 特殊输入:输入为空指针

#34 二叉树中和为定值的路径

递归;路径的判断:节点为叶节点

class Solution {
public:vector<vector<int>> res;vector<int> path;vector<vector<int>> pathSum(TreeNode* root, int sum) {if(root==NULL) return {};findPath(root, sum);return res;}void findPath(TreeNode* &root, int target){if(root == NULL) return;target -= root->val;path.push_back(root->val);if(root->left == NULL && root->right == NULL){if(target == 0) res.push_back(path);}else{if(root->left) findPath(root->left, target);if(root->right) findPath(root->right, target);}path.pop_back(); //回退}
};
测试用例:
  • 功能测试:各种形态的二叉树;树中有一个、多个、0个符合条件的路径
  • 特殊输入:nullptr

#35 复杂链表的复制

链表含有指向下一个node的指针,和指向一个随机对象的random指针

解法一:直接复制

直接复制构建链表和next指针,之后再遍历找到random

  • 时间复杂度:O(n) + O(n^2) 复制+找random
  • 空间复杂度:O(n)
解法二:空间换时间的直接复制

复制时使用hash表建立旧node和新node的映射关系,找random指针的时间复杂度退化为O(n)

class Solution {
public:Node* copyRandomList(Node* head) {if(head == NULL) return head;Node* oldLink{head};Node* pre{new Node(-1)};Node* newLink = pre; map<Node*, Node*> hashMap{};while(oldLink){newLink->next = new Node(oldLink->val);newLink = newLink->next;hashMap[oldLink] = newLink;oldLink = oldLink->next;}oldLink = head;newLink = pre->next;while(newLink){if(oldLink->random){newLink->random = hashMap[oldLink->random];}                elsenewLink->random = NULL;newLink = newLink->next;oldLink = oldLink->next;}return pre->next;}
};
  • 时间复杂度:O(n) + O(n) 复制+找random
  • 空间复杂度:O(n) + O(n) 链表+hash表
解法三:关联位置记录

将新node直接建立在旧node之后,找random指针时旧node和新node的位置时相邻的;指针找好后再将一个链表拆成两个
note:random仍然要在所有node都建立完毕再去找

class Solution {
public:Node* copyRandomList(Node* head) {if(head == NULL) return NULL;Node* cur{head};while(cur){Node* tmp = cur->next;cur->next = new Node(cur->val);cur->next->next = tmp;cur = tmp;}cur = head;while(cur){if(cur->random)cur->next->random = cur->random->next;elsecur->next->random = NULL;cur = cur->next->next; }cur = head;Node* first = head->next;Node* newCur = first;while(newCur != NULL){cur->next = newCur->next;cur = cur->next;if(cur)newCur->next = cur->next;else    newCur->next = NULL;newCur = newCur->next;}return first;}
};
  • 时间复杂度:O(n) + O(n) + O(n) 复制 + 找random + 拆表
  • 空间复杂度:O(n)
测试用例:
  • 功能测试:random指向空、指向别的节点、指向自身;形成环的节点;单节点链表
  • 特殊输入:nullptr

#36 二叉搜索树与双向链表

左子树的最右在root前,右子树最左在root后

class Solution {
public:Node* treeToDoublyList(Node* root) {if(!root) return nullptr;inorder(root);head->left = pre;pre->right = head;return head;}
private:Node* pre = nullptr; //始终保存已排序链表的最后一个Node* head = nullptr;void inorder(Node* root) {if(root == nullptr) return;if(root->left) inorder(root->left); //inorder,所以第一个时最左节点if(!pre) head = root; //头节点,只有第一次进行赋值else     pre->right = root;root->left = pre; //把pre节点和root连接在一起pre = root;if(root->right) inorder(root->right);}
};
测试用例
  • 功能测试:树;完全二叉树;仅左/右子节点
  • 特殊输入:空节点;单节点

#37 序列化和反序列化

将树序列化,再反序列化
leetcode中调用方式为code.deserialize(serialize(root)),所以序列化时使用层序、前中后序自己选择,只要最后恢复的树和原树是相同的就可以

class Codec {
public:// Encodes a tree to a single string.string serialize(TreeNode* root) {if(!root) return "";stringstream ss; //使用stringstream保存结果queue<TreeNode*> qT{}; //使用简单的层序qT.push(root);TreeNode* tmp;while(!qT.empty()){tmp = qT.front(); qT.pop();if(!tmp) ss << "$ ";else{ss << tmp->val << ' ';qT.push(tmp->left);qT.push(tmp->right);} }return ss.str(); }// Decodes your encoded data to tree.TreeNode* deserialize(string data) {if(data.empty()) return nullptr;stringstream ss(data);string t;ss >> t;TreeNode* head = new TreeNode(stoi(t));queue<TreeNode*> QT;QT.push(head);while(!QT.empty()){TreeNode* tmp = QT.front(); QT.pop();ss >> t;if(t == "$") tmp->left = nullptr;else{tmp->left = new TreeNode(stoi(t));QT.push(tmp->left);}ss >> t;if(t[0] == '$') tmp->right = nullptr;else{tmp->right = new TreeNode(stoi(t));QT.push(tmp->right);}}return head;}
};
测试用例
  • 功能测试:树;完全二叉树;仅左/右子节点
  • 特殊输入:空节点;单节点

#38 字符串的全排列

要求不能有重复元素,但是可以不按顺序

class Solution {
public:vector<string> permutation(string s) {vector<string> res{};set<string> resTmp{};if(s.size()<2) return {s};permutationCore(s, resTmp, 0);for(auto i:resTmp){res.push_back(i);}return res;}void permutationCore(string& s, set<string>& res, int pos){if(pos == s.size()){res.insert(s); //用set去重}else{for(int i = pos; i<s.size(); i++){swap(s[pos], s[i]);permutationCore(s, res, pos+1);swap(s[pos], s[i]); //复位}}}
};
测试用例
  • 功能测试:多个字符的字符串;含重复字符的字符串
  • 特殊输入:空串或一个字符的字符串
扩展:长度为m的组合

将长为n的字符分为两个部分,从第一个部分选择1个字符,第二个部分选择m-1个;或者从第一个部分选择0个,第二个部分选择m个

扩展:在正方体的八个顶点放数

使得三组相对面的顶点和相同
思路:先全排列,再判断是否满足三组顶点和相等的条件

扩展:八皇后问题

每行遍历选位置,则行数一定不同;对列进行全排列,判断是否有冲突(同行同列同对角线)

#39 数组中出现次数超过一半的数

解法一:hash方法
class Solution {
public:int majorityElement(vector<int>& nums) {int half = (nums.size()+1)/2;unordered_map<int, int> m;for(int i:nums) {++m[i];if(m[i] >= half) return i;}return 0;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
解法二:大数方法

选择一个数作为majority数,遇到相同的数则count加一,遇到不同的数则count减一;count减到0则重新赋majority值
虽然可能majority数会切换多次,且不是真正的众数,但是既然majority数出现次数时超过一半的,这样加一减一去掉的非majority数和majority数是平衡的,最终赋值的数的一定是真正的majority数

class Solution {
public:int majorityElement(vector<int>& nums) {int maj{};int count{0};for(int i:nums){maj = (count == 0)?i:maj;count = (i == maj)?count+1:count-1;}return maj;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
解法三:排序

排序后,取中间位置的数

class Solution {
public:int majorityElement(vector<int>& nums) { //空数组sort(begin(nums), end(nums));return nums[nums.size()/2];}
};
  • 时间复杂度:O(nlogn) 使用快排,最优情况下
  • 空间复杂度:O(logn) 快排递归调用带来的空间使用;实际并不使用空间
解法四:改进的排序,partion方法

不需要完整进行排序,只要partion返回的数的位置在中间位置就行

class Solution {
public:int majorityElement(vector<int>& nums) { int half = nums.size()/2;int end = nums.size()-1;int pivot = -1; //起始值-1或size();不能用0哦,会把第一个数省略不排序while(pivot != half) {if(pivot > half) pivot = partion(0, pivot-1, nums);else pivot = partion(pivot+1, end, nums);}return nums[pivot];}int partion(int begin, int end, vector<int>& nums) {int left = begin + 1, right = end;int pivot = nums[begin];while(true) {while(left <= right && nums[left] < pivot) ++left;while(left <= right && nums[right] >= pivot) --right;if(left > right) break;swap(nums[left], nums[right]);}swap(nums[right], nums[begin]);return right;}
};
  • 时间复杂度:O(nlogn) 超时未通过,数字均相同的情况下返回的位置偏移中心位置;需要改进
测试用例
  • 功能测试:存在/不存在出现超过一半数字的数
  • 性能测试:数组长度较大时;数字均相同且数组长度极大的数组;
  • 特殊输入:空数组;单个数字数组

#40 最小的k个数

不要求返回的数组排序

解法0:暴力法

先排序,再返回前k个数;如果使用快排,则时间复杂度O(n*logn)

解法一:优先队列

维持一个大小为k的优先队列(基于大根堆),每次pop出最大的数;返回的数组时有序的

class Solution {
public:vector<int> getLeastNumbers(vector<int>& arr, int k) {if(arr.size() <=k ) return arr;priority_queue<int> pq;for(int i : arr) {if(pq.size() < k) pq.push(i);else {pq.push(i);pq.pop();}}vector<int> res;while(!pq.empty()) {res.push_back(pq.top());pq.pop();} //priotity_queue不提供遍历和迭代器,笨方法取出return res;}
};
  • 时间复杂度:O(nlogk)
  • 空间复杂度:额外需要O(k)的优先队列
解法二:partion方法

需要修改数组元素

class Solution {
public:vector<int> getLeastNumbers(vector<int>& arr, int k) {if(arr.size() <=k ) return arr;int pos = arr.size();while(pos != k){if(pos > k) pos = partion(arr, 0, pos-1);else pos = partion(arr, pos+1, arr.size()-1);}vector<int> res(arr.begin(), arr.begin()+pos);return res;}int partion(vector<int>& arr, int start, int end){int left = start+1, right = end;int pivot = arr[start];while(true){while(left <= right && arr[left] < pivot) left++;while(left <= right && arr[right] >= pivot) right--;if(left>right) break;swap(arr[left], arr[right]);}swap(arr[right], arr[start]);return right;}
};
  • 时间复杂度:O(nlogn)
测试用例
  • 功能测试:数组中存在/不存在相同数字
  • 边界测试:输入k = 1或k = size()
  • 特殊输入:k = 0; k > size(); size() = 0

#41 数据流中的中位数

输入数据,返回中位数

解法一:vector暴力法

插入时排序;返回中间数
已排序数组值的插入:二分找到第一个比target大的数,再插入

class MedianFinder {
public:/** initialize your data structure here. */MedianFinder() {}void addNum(int num) {if(container.empty() || num>container.back()) {container.push_back(num);return;}int p = findPos(num);container.insert(container.begin()+p, num);}double findMedian() {if(container.empty()) return 0;int len = container.size();return len%2?container[len/2]:container[len/2]*0.5 + container[len/2-1]*0.5;}
private:vector<int> container;int findPos(int num) {int l = 0, r = container.size()-1;while(l < r) {int mid = l + (r-l)/2;if(container[mid] < num) l = mid+1;else r = mid;}return l;}
};
  • 时间复杂度:O(logn) + O(n) 二分+插入;使用sort函数则复杂度O(nlogn)
  • 空间复杂度:O(n)
解法二:堆

数据流的保存方式和顺序不受限制;不要求存取其他数,可以使用一个最大堆、一个最小堆,分布保留两个中位数

class MedianFinder {
public:/** initialize your data structure here. */MedianFinder() {}void addNum(int num) {maxHeap.push(num);int tmp = maxHeap.top();maxHeap.pop();minHeap.push(tmp); //保持两边堆数值平衡if(maxHeap.size()<minHeap.size()){maxHeap.push(minHeap.top());minHeap.pop();} //保持堆大小平衡}double findMedian() {if(minHeap.empty() && maxHeap.empty()) return 0;return maxHeap.size()>minHeap.size()?maxHeap.top():(maxHeap.top()+minHeap.top())*0.5;}
private:priority_queue<double> maxHeap;priority_queue<double, vector<double>, greater<double>> minHeap;
};
  • 时间复杂度:插入:O(logn) 查找:O(1)
  • 空间复杂度:O(n)
测试用例
  • 功能测试:偶数数据流;奇数数据流
  • 特殊输入:0、1个数字

#42 连续子数组的最大和

解法0:暴力法

两个循环,遍历所有字数组

  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n^2)
解法一:动态规划

dp[i]意味着以i结尾的字数组的最大和;如果dp[i-1]<0,则i单列数组,将前面的数包含进来不会对增加数组和有帮助

class Solution {
public:int maxSubArray(vector<int>& nums) {if(nums.empty()) return 0;int len = nums.size();vector<int> dp(len+1, 0);int res{INT_MIN};for(int i = 1; i <= len; ++i) {if(dp[i-1] < 0) dp[i] = nums[i-1];else dp[i] = dp[i-1] + nums[i-1];res = max(res, dp[i]);}return res;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
解法二:贪心/空间优化的动态规划
class Solution {
public:int maxSubArray(vector<int>& nums) {if(nums.empty()) return 0;int sum{0};int maxSum{INT_MIN};for(int i:nums){if(sum < 0) sum = i;else sum += i;if(sum > maxSum) maxSum = sum;}return maxSum;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例
  • 功能测试:有正有负;全正;全负
  • 特殊输入:空指针

#43 1~n中1出现的次数

解法一:暴力法

从1到n,循环除下去判断有多少位为1

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(1)
解法二:找规律

按位判断,每位可能取1的个数
e.g. xyztabc

  • 前三位取0~(xyz-1)时,t位1的个数:xyz*100;
  • 前三位取xyz,t位1的个数与t的大小有关
    • t = 0:0个
    • t = 1:abc个
    • t > 1: 1000个
class Solution {
public:int countDigitOne(int n) {int count{0};for(int k = 1; k <= n; k*=10){int abc = n%k;int xyzt = n/k;int t = xyzt%10;int xyz = (xyzt+8)/10; //t>1时,+1000,直接包含在h中count += xyz*k;if(t == 1) count += abc + 1; if(xyz == 0) break;}return count;}
};
测试用例
  • 功能测试:数字
  • 性能测试:大数
  • 特殊输入:0,1等

#44 数字序列中某一位的数字

找规律 012345678910111213…

  • 先确定区间 几位数
  • 再确定哪个数
  • 再锁定该数的具体哪一位值
class Solution {
public:int findNthDigit(int n) {int i =1;for(; i*9*pow(10, i-1)<n; i++){n -= i*9*pow(10, i-1);};n--;//减去0int num = pow(10,i-1) + n/i;  //对应的具体数字string a = to_string(base);   //将数字变为string,可以通过下标取数字;return (a[n%i]-'0');     }
};
测试用例
  • 功能测试:不同n值,对应一位、两位、三位数字中的一位
  • 性能测试:大数
  • 边界测试:0、2^31

#45 把数组排成最小数

输出字符串
核心思想:排序
从前向后按位比较;简单方式p1p2<p2p1,则p1应该放在p2前面,e.g. 1213<1312,12在13前面

解法一:
class Solution {
public:string minNumber(vector<int>& nums) {string res{};if(nums.empty()) return res;vector<string> s_num;for(int i: nums) s_num.push_back(to_string(i));auto compare = [](const string &p1, const string &p2){return p1+p2 < p2+p1;};sort(s_num.begin(), s_num.end(), compare);for(string s:s_num) res += s;return res;}
};
测试用例
  • 功能测试:各种排列的数组
  • 特殊输入:空数组

#46 数字翻译成字符串

输出有多少种翻译方法

解法一:动态规划
class Solution {
public:int translateNum(int num) {if(!num || !(num/10)) return 1;string s = to_string(num);int len = s.length();vector<int> dp(len+1);dp[0] = 1;dp[1] = 1;for(int i = 2; i<=len; ++i) {dp[i] += dp[i-1]; //当前位单独翻译int low = s[i-1]-'0'; //当前位连着前一位一起翻译int high = s[i-2]-'0';int cur = high*10 + low;if(cur < 26 && high != 0) dp[i] += dp[i-2]; //首位不为零且小于26的两位数}return dp[len];}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
解法二:动态规划的空间优化
class Solution {
public:int translateNum(int num) {if(!num || !(num/10)) return 1;string s = to_string(num);int len = s.length();int dp0 = 1, dp1 = 1;for(int i = 2; i<=len; ++i) {int dp = dp1;int low = s[i-1]-'0';int high = s[i-2]-'0';int cur = high*10 + low;if(cur < 26 && high != 0) dp += dp0;dp0 = dp1;dp1 = dp;}return dp1;}
};
测试用例
  • 功能测试:一位数字;多位数字
  • 特殊输入测试/边界:负数;0;26

#47 礼物的最大值

解法一:动态规划
class Solution {
public:int maxValue(vector<vector<int>>& grid) {if(grid.empty() || grid[0].empty()) return 0;vector<vector<int>> dp(grid.size()+1, vector<int>(grid[0].size()+1, 0));for(int i = 1; i<=grid.size();i++){for(int j = 1; j<=grid[0].size(); j++){dp[i][j] = max(dp[i][j-1], dp[i-1][j]) + grid[i-1][j-1];}}return dp[grid.size()][grid[0].size()];}
};
  • 时间复杂度:O(mn)
  • 空间复杂度:O(mn)
解法二:动态规划的空间优化

只需要用到上一行/上一列的结果

class Solution {
public:int maxValue(vector<vector<int>>& grid) {if(grid.empty() || grid[0].empty()) return 0;vector<int> dp(grid[0].size()+1, 0); //横向,也可以竖向for(int i = 1; i<=grid.size(); ++i){for(int j =1; j<=grid[0].size(); ++j){dp[j] = max(dp[j-1], dp[j]) + grid[i-1][j-1];}}return dp.back();}
};
  • 时间复杂度:O(mn)
  • 空间复杂度:O(n),如果竖向压缩,则为O(m)
测试用例
  • 功能测试:多行多列矩阵
  • 边界测试:单行单列矩阵;单数矩阵
  • 特殊输入:空矩阵

#48 最长不含重复字符的子字符串

解法一:暴力法

两个循环,变量所有的ij区间,判断是否有重复数组

  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)
解法二:滑动窗口

使用set辅助判断有否有重复字符

class Solution {
public:int lengthOfLongestSubstring(string s) {if(s.size() == 0) return 0;unordered_set<char> window;int maxStr = 0; int left = 0;for(int i = 0; i < s.size(); i++){char index = s[i];while (window.find(s[i]) != window.end()){ //如果之前该字符已经出现,则移动滑动窗口的左端到该重复字符的后一位//窗口内的字符始终是不重复的,即重复字符最多只出现一次,因此可以用set来进行存储window.erase(s[left]);left++;} maxStr = max(maxStr,i-left+1); //i-left+1表示当前window的大小window.insert(s[i]); //去重判断结束后再插入,set不允许重复}return maxStr;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:最差O(n),但是因为字符最多只有256种,因此可以认为是常数空间
解法三:动态规划

本质上也是滑动窗口;
使用数组辅助记录上一个该字符出现的位置

  • 如果小于当前窗口,则窗口左边收缩;
  • 如果该字符没有出现过,或者上次出现在窗口外,则右侧可以正常扩展一位
class Solution {
public:int lengthOfLongestSubstring(string s) {if(s.size() < 2) return s.size();int sLen = s.length();vector<int> pos(256, -1);int maxLen = -1;vector<int> dp(sLen+1, 0);dp[0] = 0;for(int i=1; i<=sLen; i++){int index = s[i-1]; //当前字符int d = i - pos[index];if(pos[index] == -1 || d > dp[i-1]) dp[i] = dp[i-1] + 1;else dp[i] = d;pos[index] = i; //更新出现位置if(dp[i] > maxLen) maxLen = dp[i];}return maxLen;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(256)可以认为常数空间
测试用例
  • 功能测试:字符串
  • 特殊输入:空字符串,单字符字符串
  • 性能测试:长字符串

#49 丑数

解法零:暴力法

逐个判断num是否为丑数,是则++

  • 时间复杂度:很大
  • 空间复杂度:O(1)
解法一:动态规划

主动构造丑数;空间换时间
使用三个指针,每个指针指向的数每轮仅乘相应的数,即2、3或者5

//如果调用方式是对一个对象多次操作,这样可以避免多次计算;也可以定义静态成员,让所有类成员共享
class Solution {
public:int nthUglyNumber(int n) {generator(n);return nums[n-1];}
private:vector<int> nums{1};int p2 = 0;int p3 = 0;int p5 = 0;void generator(int n) {int p = nums.size();if(n < p) return;while(p < n){double tmp = min(min(nums[p2]*2, nums[p3]*3), nums[p5]*5);nums.push_back(tmp);if(tmp == (long)nums[p2]*2) p2++;if(tmp == (long)nums[p3]*3) p3++;if(tmp == (long)nums[p5]*5) p5++;//用if而不是if else,因为可能两个指针得到相同的数//指针++不仅是跳过了当前这位的意思,而是前面所有数都不可能产生更大的丑数了,因此这种方式不会产出重复++p;}}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
测试用例
  • 功能测试:正常的n
  • 边界测试:0,1
  • 性能测试:n很大时

#50 第一个只出现一次的字符

解法一:暴力法

两个循环,对每个字符,都从头到尾统计其出现次数

  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)
解法二:数组换时间

改变顺序,先统计次数,再定位字符

class Solution {
public:char firstUniqChar(string s) {int cnt[52] = {0};for(char i:s){cnt[i-'a']++;}for(auto i:s){ //第一个,所以用s开始遍历if(cnt[i-'a'] == 1) return i;}return ' ';}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
测试用例
  • 功能测试:不存在只出现一次的字符,多个只出现一次的字符
  • 性能测试:字符很长
  • 特殊输入:空字符

#51 数组中的逆序对

解法一:

分治,涉及大小的判断,可以直接使用归并算法的模板; 归并算法中使用两个数组交换的方式,否则就需要先把之前排序玩的两个分数组预先保存

class Solution {
public:int reversePairs(vector<int>& nums) {if(nums.size()<2) return 0;vector<int> Sort(nums.begin(), nums.end());int cnt = reversePairsCore(Sort, nums, 0, nums.size()-1);        return cnt;}int reversePairsCore(vector<int>& nums, vector<int>& Sort, int start, int end){if(start > end) return 0;if(start == end){Sort[start] = nums[start];return 0;}int mid = (start + end)/2;int leftCnt = reversePairsCore(Sort, nums, start, mid);int rightCnt = reversePairsCore(Sort, nums, mid+1, end);int cnt(0);int l_Pos = mid;int r_Pos = end;int index = end;while(l_Pos >= start && r_Pos >= mid+1){if(nums[l_Pos] > nums[r_Pos]){cnt += r_Pos - mid;Sort[index--] = nums[l_Pos--];}else {Sort[index--] = nums[r_Pos--];}}while(l_Pos >= start){Sort[index--] = nums[l_Pos--];}while(r_Pos >= mid+1){Sort[index--] = nums[r_Pos--];}return leftCnt + rightCnt + cnt;}
};
  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(n)
测试用例
  • 功能测试:
  • 性能测试:
  • 特殊输入/边界:

#52 两个链表的第一个公共节点

解法一:暴力法

对链A的每一个节点,遍历链B找是否有相同值
节点转存到数组,然后从后向前遍历

解法一:打结

把A的结尾连到B,B的结尾连到A,每个指针都会走A+B-C长度的距离后相遇

  • C为相同节点链的长度
  • 如果没有公共节点,C = 0,则两个指针会同时在nullptr相遇
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例
  • 功能测试:
  • 性能测试:
  • 特殊输入/边界:

#53 排序数组中查找数字

输出某数字出现的次数

解法一:双指针,求上下边界
class Solution {
public:int search(vector<int>& nums, int target) {if(nums.empty() || nums[0] > target || nums[nums.size()-1] < target) return 0;int lower_bound{0}, upper_bound{0};int l = 0, r = nums.size()-1;while(l <= r){int mid = (l + r)/2;if(nums[mid]>=target) r = mid-1;else l = mid+1;}lower_bound = r;l = 0, r = nums.size()-1;while(l <= r){int mid = (l+r)/2;if(nums[mid]>target) r = mid-1;else l = mid+1;}//为了不陷入死循环,=的情况一般要多移一位//二分法把大于、小于、等于的情况分开分析,会更清晰upper_bound = l;return upper_bound-lower_bound-1;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例
  • 功能测试:不存在该数;存在该数,出现一次/多次
  • 性能测试:数组很长
  • 特殊输入/边界:空数组;所有数相同的数组
扩展题目:0~n-1中缺失的数字

0~n-1按序排列,仅缺失一个数字
对比下标即可

class Solution {
public:int missingNumber(vector<int>& nums) {if(nums.empty()) return 0;int r = nums.size();int l = 0;while(l < r){int mid = (l+r)/2;if(nums[mid] == mid) l = mid+1;else if(nums[mid] > mid) r = mid;}return r;}
};

#54 二叉搜索树的第k大节点

倒序的前序遍历,得到从大到小排列

解法一:
class Solution {
public:int kthLargest(TreeNode* root, int k) {if(k == 0) return 0;TreeNode* p = root;stack<TreeNode*> s;while(p || !s.empty()){while(p){s.push(p);p = p->right; //推右节点,和前序相反}if(!s.empty()){p = s.top(); s.pop();if(k == 1) return p->val;k--;p = p->left;}}return -1;}
};
  • 时间复杂度:O(logn)
  • 空间复杂度:O(n)
测试用例
  • 功能测试:各种形状的树
  • 特殊输入/边界:空树

#55 二叉树的深度

解法一:递归

max(左子树的深度, 右子树的深度) + 1

class Solution {
public:int maxDepth(TreeNode* root){return (!root)?0:max(maxDepth(root->left), maxDepth(root->right))+1;}
};
  • 时间复杂度:O(logn)
  • 空间复杂度:O(logn)
解法二:循环

借用栈,推入栈的节点用pair记录当前深度

class Solution {
public:int maxDepth(TreeNode* root) {if(!root) return 0;stack<pair<TreeNode*, int>> q;q.push({root, 1});int maxLen{0};while(!q.empty()){TreeNode* tmp = q.top().first;int curLen = q.top().second; q.pop();if(tmp){maxLen = max(curLen, maxLen);cout << tmp->val;if(tmp->right) q.push({tmp->right, curLen+1});if(tmp->left) q.push({tmp->left, curLen+1});}}return maxLen;}
};

层序遍历 + 统计循环次数

class Solution {
public:int maxDepth(TreeNode* root) {if(!root) return 0;queue<TreeNode*> q;q.push(root);int depth{0};while(!q.empty()){int loop = q.size();while(loop--){TreeNode* tmp = q.front(); q.pop();if(tmp->left) q.push(tmp->left);if(tmp->right) q.push(tmp->right);cout << tmp->val;}depth++;}return depth;}
};
测试用例
  • 功能测试:各种形状的树
  • 特殊输入/边界:空树
平衡二叉树:

左右子树深度差不过1
递归判断子树也为平衡树时,顺便求深度

class Solution {
public:bool isBalanced(TreeNode* root) {if(!root) return true;int depth{0};return isBalanced(root, &depth);}bool isBalanced(TreeNode* root, int* depth){//跨函数传参,可以指针,或者直接用引用if(!root){*depth = 0;return true;}  int left{0}, right{0}; if(isBalanced(root->left, &left) && isBalanced(root->right, &right)){int diff = abs(left-right);*depth = max(left, right)+1;if(diff <2) return true;}return false;}
};

#56 数组中出现1次的两个数

除了两个数出现一次外,其他数出现两次

解法一:hash

统计次数

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
解法二:异或
  • 自身异或得到0
  • 异或时,不同的值得到1
  • a&(-a) 得到最地位的1
class Solution {
public:vector<int> singleNumbers(vector<int>& nums) {if(nums.size() <= 2) return nums;int resTwo{0};for(int i:nums){resTwo ^=i;}int last = resTwo & (-resTwo); //得到两个数的不同位vector<int> res(2,0);for(int i:nums){if(i&last) res[0] ^= i;else res[1] ^=i;}return res;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例
  • 功能测试:数组;
扩展题目

数组中一个数出现一次,其他数出现3次
按bit位求和,出现三次,因此一定能被3整除;整除后剩下的数则均属于出现一次的数

class Solution {
public:int singleNumber(vector<int>& nums) {if(nums.empty()) return 0;vector<int> digits(32, 0);for(int i:nums){int mask = 1;for(int j = 31; j>0; j--){if(i&mask) digits[j] += 1;mask = mask << 1;}}int res{0};for(int i = 0; i<32; i++){res = res << 1;res += digits[i]%3;}return res;}
};

#57 和为s的两个数字

已排序数组
双指针

class Solution {
public:int singleNumber(vector<int>& nums) {if(nums.empty()) return 0;vector<int> digits(32, 0);for(int i:nums){int mask = 1;for(int j = 31; j>0; j--){if(i&mask) digits[j] += 1;mask = mask << 1;}}int res{0};for(int i = 0; i<32; i++){res = res << 1;res += digits[i]%3;}return res;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例
  • 功能测试:存在/不存在和为target的两个数
  • 特殊输入/边界:空指针
扩展题目:和为s的连续正数序列

输入target,数组为1~n均匀排列

class Solution {
public:vector<vector<int>> findContinuousSequence(int target) {int small = 1, big = 2;int middle = (target+1)/2;int sum = 3;vector<vector<int>> res{};while(small < middle){while(small < middle && sum > target){sum -= small;small++;}if(sum == target) res.push_back(print(small, big));    big++;sum += big;}return res;}vector<int> print(int begin, int end){vector<int> res{};for(int i = begin; i<=end; i++){res.push_back(i);}return res;}
};

#58 翻转字符串的单词

hello world->world hello
注意判断中间的、首位的多个空格

解法一:栈

分切,丢进栈里,再依次取出

class Solution {
public:string reverseWords(string s) {stringstream ss(s);stack<string> sstack{};string t;while(ss >> t){sstack.push(t);cout << t << " ";}t = "";while(!sstack.empty()){t += sstack.top() + ' ';sstack.pop();}t.pop_back();return t;}
};
解法二:翻转

先整体翻转字符,再单词翻转,得到结果

class Solution {
public:string reverseWords(string s) {auto start = s.begin();auto end = s.begin() + (s.size() - 1);while(*start == ' ') start++;while(*end == ' ') end--;reverse(start, end+1);auto cur = start;string word{};while(cur <= end){auto iter = cur;while(cur <= end && *cur != ' '){cur++;}reverse(iter, cur);word += string(iter, cur) + ' ';while(cur <= end && *cur == ' ') cur++;}word.pop_back(); //丢掉最后一个空格return word;}
};
测试用例
  • 功能测试:多个单词;一个单词
  • 特殊输入/边界:空
扩展题目:左旋转字符串

从n的位置旋转字符串

class Solution {
public:string reverseLeftWords(string s, int n) {if(n <=0) return s;reverse(s.begin(), s.begin()+n);reverse(s.begin()+n, s.end());reverse(s.begin(), s.end());return s;}
};

#59 滑动窗口的最大值

解法一:deque

类似含min的栈,依次pop直到剩下的数不小于自己;deque内部非严格递减

class Solution {
public:vector<int> maxSlidingWindow(vector<int>& nums, int k) {if(nums.empty()) return {};deque<int> max{};vector<int> res{};for(int i = 0; i<nums.size(); i++){while(!max.empty() && nums[max.back()] < nums[i]) max.pop_back();max.push_back(i); //deque存下标,就可以判断是否在当前窗口内,从而进行删除while(max.front() < i-k+1) max.pop_front();if(i>k-2 && !max.empty()) res.push_back(nums[max.front()]);}return res;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
解法二:双数组
  • 将数组按k分组
  • left存储从左向右的最大值;right存储从右向左的最大值
class Solution {
public:vector<int> maxSlidingWindow(vector<int>& nums, int k) {if(nums.empty()) return {};vector<int> left(nums.size());vector<int> right(nums.size());for(int i = 0; i<nums.size(); i++){if(i%k == 0) left[i] = nums[i];else left[i] = max(left[i-1], nums[i]);int j = nums.size()-1-i;if((j+1)%k == 0 || j == nums.size()-1) right[j] = nums[j];else right[j] = max(right[j+1], nums[j]);}vector<int> res(nums.size()-k+1, 0);for(int i = 0; i<res.size(); i++) res[i] = max(right[i], left[i+k-1]); return res;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
测试用例
  • 功能测试:数组
  • 特殊输入/边界:空数组
题目扩展:队列的最大值

和上面类似,存储时保证deque非严格递增;

pop时,如果此时max首部和删除的值相同,则进行删除;注意仅删除一次,可能存在相等值

class MaxQueue {
public:MaxQueue() {}int max_value() {if(max.empty()) return -1;return max.front();}void push_back(int value) {data.push(value);while(!max.empty() && max.back() < value) max.pop_back();max.push_back(value);}int pop_front() {int n{-1};if(data.empty()) return n;n = data.front();data.pop();if(max.front() == n) max.pop_front();return n;}
private:queue<int> data{};deque<int> max{};
};

#60 n个筛子的点数

找规律,核型递增的出现次数

解法一:动态规划

n个筛子得到x点的可能数 = sum(n-1个筛子得到x-1、x-2、… 、x-6)

空间优化

class Solution {
public:vector<double> twoSum(int n) {vector<double> res(6*n+1, 0);res[0] = 1;for(int i = 1; i <= n; i++){for(int j = 6*i; j>=i; j--){res[j] = 0;for(int x = j-1; x >= i-1 && x >= j-6; x--) res[j] += res[x];}}for(auto &num: res) num = num/pow(6,n); //返回结果要求概率return vector<double>(res.begin()+n, res.end());}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例
  • 功能测试:不同的n
  • 性能测试:n很大
  • 特殊输入/边界:n=0

#61 扑克牌中的顺子

大小王可以当作任意牌

统计确少多少个牌,手里的大小王是否足够填补缺

class Solution {
public:bool isStraight(vector<int>& nums) {if(nums.size() <5) return false;int cnt0{0};int missing{0};sort(nums.begin(), nums.end()); //先排序for(int i = 0; i<nums.size(); i++){if(nums[i] == 0) cnt0++;else if(i>0 && nums[i-1] != 0){int diff = nums[i]-nums[i-1];if(diff == 0) return false; //有相同的数,则直接返回falsemissing += diff-1;}}if(cnt0 >= missing) return true;return false;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例
  • 功能测试:各种牌
  • 特殊输入/边界:空

#62 圆圈中剩下的数字

解法一:链表方法

超时 + 没有充分利用条件0~n-1的数字

class Solution {
public:int lastRemaining(int n, int m) {if(n < 2) return 0;list<int> num;for(int i = 0; i<n; i++) num.push_back(i);while(num.size()>1) {for(int i = 0; i<m; ++i) {if(i != m-1) num.push_back(num.front());num.pop_front();}}return num.front();}
};
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n)
解法二:约瑟夫环-数学方法-动态规划

找出每轮删除的数字的规律
每轮删除后,iter后移,相当于整个数组向前搬移了m位(把数字看成位置);找一共k位删除剩余数字和k-1剩余数字的关系
得到k-1位数字删除后剩余的数字,假设为a,k为数字经过一次删除后,落到对应k-1个数字a位上的数字,就是最后剩下的数字

class Solution {
public:int lastRemaining(int n, int m) {if(n < 2) return 0;int last = 0; //1个数时,即剩余0for(int i = 2; i<=n; i++){last = (last + m)%i; //上一列的位置上映射的是(last+m)%i这个数字}return last;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例
  • 功能测试:n
  • 性能测试:n很大
  • 特殊输入/边界:0、1

#63 股票的最大利润

一次买卖可以得到的最大利润

解法一:动态规划

维护一个数组,记录到当前位的最小值,作为买入价;和当前位相减得到收益

class Solution {
public:int maxProfit(vector<int>& prices) {if(prices.size() <= 1) return 0;int maxV{0}, minV{INT_MAX};for(int i = 1; i<prices.size(); i++){minV = min(minV, prices[i-1]);maxV = max(maxV, prices[i]-minV);}return maxV;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例
  • 功能测试:数组;单增/单减/不变数组
  • 特殊输入/边界:空数组

#64 计算sum(1, … , n)

不能使用乘除法、if else、switch等条件判断语句

解法一:断点效应

计算方法sum的方法:迭代、公式、递归
迭代需要循环;公式需要乘除法;递归需要终止条件的判断
最容易满足的是递归的终止条件判断;通过断点效应实现

class Solution {
public:int sumNums(int n) {n && (n += sumNums(n-1));return n;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n) 递归调用的空间开销
测试用例
  • 功能测试:n
  • 性能测试:大n
  • 特殊输入/边界:1,0

#65 不用加减做加法

加法按位来看且不考虑进位,结果和异或是相同的;而进位结果可以使用与&来实现
因此可以使用与和异或来代替加法计算

class Solution {
public:int add(int a, int b) {while(b){int carry = unsigned(a&b) << 1;a ^= b;b = carry;}return a;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例
  • 功能测试:a/b
  • 特殊输入/边界:0/0

#66 构建乘积数组

对应成绩数组的值为除了当前位之外,所有位的乘积;要求不能使用除法

解法一:构建两个数组,从前向后的乘积/从后向前的乘积
class Solution {
public:vector<int> constructArr(vector<int>& a) {if(a.empty()) return {};vector<int> front(a.size(), 1);vector<int> tail(a.size(), 1);for(int i = 1; i<front.size();i++) front[i] = front[i-1]*a[i-1];for(int i = tail.size()-2; i>=0; i--) {tail[i] = tail[i+1]*a[i+1];}for(int i = 0; i<front.size();i++) front[i] *= tail[i];return front;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
测试用例
  • 功能测试:不同的输入
  • 特殊输入/边界:空

#67 把字符串转换成数字

溢出时返回INT_MAX或INT_MIN
注意:

  • 去掉首部空格,及首部+/-号
  • 处理溢出
  • 遇到非数字位终止
class Solution {
public:int strToInt(string str) {int start{0};int num{0};int flag{+1};while(str[start] == ' ') {start++;}if(str[start] == '-') flag = -1;if(str[start] == '-' || str[start] == '+') start++; while(start < str.size() && isdigit(str[start])){int n = str[start] - '0';if( num>INT_MAX/10 || (num==INT_MAX/10 && n >7)) return flag > 0 ? INT_MAX:INT_MIN;num = num*10 + n;start++;}return num*flag;}
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
测试用例
  • 功能测试:带各种异常的str
  • 特殊输入/边界:空

#68 二叉搜索树的最近公共祖先

公共祖先满足值位于两个数之间

class Solution {
public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {if(!root) return nullptr;TreeNode* cur = root;int t1 = min(p->val, q->val);int t2 = max(p->val, q->val);while(cur){int curValue = cur->val;if(curValue <= t2 && curValue >= t1) return cur;if(curValue > t2) cur = cur->left;else if(curValue < t1) cur = cur->right;}return nullptr;}
};
  • 时间复杂度:O(logn)
  • 空间复杂度:O(1)
测试用例
  • 功能测试:不同的树/不同的输入节点
  • 特殊输入/边界:空树,空节点
题目扩展:二叉树的最近公共祖先

递归判断,子树是否含有两个节点;

class Solution {
public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {if(!root) return nullptr;if(root == p || root == q) return root;TreeNode* left = lowestCommonAncestor(root->left, p, q);TreeNode* right = lowestCommonAncestor(root->right, p,q);if(!left) return right;if(!right) return left;if(left && right) return root;return nullptr; }
};

[刷题]剑指offer C++语言刷题-多解法尝试相关推荐

  1. 《剑指 Offer I》刷题笔记 11 ~ 19 题

    <剑指 Offer I>刷题笔记 11 ~ 19 题 查找算法(中等) 11. 二维数组中的查找 _解法 1:暴力迭代 解法 2:标志数 解法 3:逐行二分 12. 旋转数组的最小数字 _ ...

  2. 《剑指 Offer I》刷题笔记 1 ~10 题

    <剑指 Offer I>刷题笔记 1 ~10 题 栈与队列(简单) 1. 用两个栈实现队列 _解法 1:暴力做法 解法 2:优化解法 1 2. 包含 min 函数的栈 _解法 1:pop( ...

  3. C#LeetCode刷题-剑指Offer

    本文由 比特飞 原创发布,欢迎大家踊跃转载. 转载请注明本文地址:C#LeetCode刷题-剑指Offer | .Net中文网. C#LEETCODE刷题概述 概述 所有LeetCode剑指Offer ...

  4. 《剑指 Offer I》刷题笔记 51 ~ 61 题

    <剑指 Offer I>刷题笔记 51_60 位运算(简单) 51. 二进制中 1 的个数 _解法1:逐伟判断 解法2:巧用 n&(n-1) 52. 不用加减乘除做加法(背题) 解 ...

  5. 《剑指 Offer I》刷题笔记 41 ~ 50 题

    <剑指 Offer I>刷题笔记 41_50 排序(中等) 41. 最小的k个数# _解法1:排序 API + 数组复制 API 42. 数据流中的中位数 _解法1:暴力 搜索和回溯算法( ...

  6. 《剑指 Offer I》刷题笔记 20 ~ 30 题

    <剑指 Offer I>刷题笔记 20_30 动态规划(简单) 20. 斐波那契数列 _解法1:迭代 解法2:记忆化递归 解法3:动态规划 21. 青蛙跳台阶问题 _解法1:动态规划 22 ...

  7. 《剑指Offer》Java刷题 NO.36 两个链表的第一个公共结点(链表,等长拼接法,长者先行法,辅助栈)

    <剑指Offer>Java刷题 NO.36 两个链表的第一个公共结点(链表,等长拼接法,长者先行法,辅助栈) 传送门:<剑指Offer刷题总目录> 时间:2020-06-19 ...

  8. LeetCode刷题剑指 Offer 10- II. 青蛙跳台阶问题

    LeetCode刷题剑指 Offer 10- II. 青蛙跳台阶问题 动态规划的思想,青蛙每次可以跳一个或两个台阶,要想得到最终青蛙跳n阶台阶的方法数,需要找到青蛙跳n-1个台阶的方法数和跳n-2个台 ...

  9. 刷《剑指offer》的感受

    刷<剑指Offer>的感受 我的刷题感受 因为大型公司笔试面试的需要,我决定开始慢慢刷题,每天累积一点,问了一些学长刷什么样的题目比较好,有些搞过ACM的学长建议leetcode,leet ...

最新文章

  1. Java 实现 SSH 协议的客户端登录认证方式--转载
  2. Hadoop集群扩容和缩容:添加白名单和黑名单
  3. 在没有任何数据时进行无效的读取尝试。_技术转载——JVM运行时内存是怎么分布的?...
  4. 经典的X/OpenDTP事务模型
  5. Linux 搭建Zookeeper集群
  6. C# 各种字符串格式
  7. RabbitMQ (一) MQ介绍以Linux下RabbitMq环境安装
  8. IComparable和Icomparer接口
  9. 学校计算机考察内容是什么意思,2019考研计算机复试四项考察内容分析及注意事项...
  10. spingMVC拦截器 -单个、多个、设想
  11. openCVPracticalExercise学习笔记04
  12. 《单细胞生物》教学反思
  13. Filter_Listener:过滤器和监听器
  14. 【Brazilian ICPC Regionals - 2018】Soteros【树上启发式合并】
  15. J1939入门(一)
  16. hen Content must be served over https解决方案
  17. 【Excel 教程系列第 1 篇】删除所有空白行,隐藏空白行
  18. 基于stm32单片机的空气质量检测仿真(仿真+源码+全套资料)
  19. 马尔科夫链细致平衡条件
  20. 亿图图示--工业自动化模块--啤酒生产处理流程简图和热水冷凝处理架构

热门文章

  1. java 面试题合集_撩课-Java面试题合辑1-50题
  2. tensorflow语义分割计算mIoU时忽略某一类别
  3. Java培训:Java枚举是什么
  4. TestBird《2021中国手游测试白皮书》---国内手游
  5. 上方网首发:TestBird《2015年度手游测试白皮书》
  6. Python spiders基础学习笔记
  7. 基于C语言的9*9数独生成器(回溯法)
  8. 《越狱》中的项目管理——两个版本的对比
  9. 新书推荐:iOS Swift 游戏开发指南
  10. 卡路里与脂肪重量的换算