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

自己刷题时的代码,一般会尝试多种解法,都是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);//获取内存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

0123
2310
1320
3120
0123
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]
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; 
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值