自己刷题时的代码,一般会尝试多种解法,都是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;
}
问题:内存读写的乱序执行
新建对象的步骤:
- 分配Singleton类型对象所需内存
- 在内存处构造类型的对象
- 把分配的内存的地址赋给指针
实际执行中,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);//获取内存fence
if (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);//释放内存fence
m_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];
else
swap(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();
} //取完仍然为空,则说明队列内无数据,返回-1
return 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,可以拆成两个2
n-=4;
res = 4;
}
if(n%3 == 2){ //余2,只能拆成一个2
n-=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 toBeDelete
curNode = 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]
- 如果p的前一位和当前位是匹配的(
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次
}
else
return 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];
}
else
newLink->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;
else
cur->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--;//减去0
int 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; //有相同的数,则直接返回false
missing += 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个数时,即剩余0
for(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;
}
};