剑指offer刷题笔记(C++版本)

目录

一、数组

         1、数组中重复的数字

2、二维数组的查找

3、数字在排序数组中出现的次数

4、旋转数组的最小数字

5、调整数组顺序使奇数位于偶数前面

6、顺指针打印矩阵

7、数组中出现次数超过一半的数字

8、连续子数组的最大和

9、数组中的逆数对

10、数组中只出现过一次的数字

11、把数组排成最小的数

12、构建乘积数组

二、链表

1、从尾到头打印链表

2、链表中倒数第K个节点

3、反转链表

4、合并两个排序链表

5、两个链表的第一个公共节点

6、链表中环的入口节点

7、删除链表中重复的节点

8、复杂链表的复制

三、字符串

1、替换空格

2、字符串的排列

3、第一个只出现一次的字符

4、左旋转字符串

5、反转单词序列

6、把字符串转换成整数

7、正则表达式匹配

8、表示数值的字符串

9、字符流中第一个不重复的字符

四、栈和队列

1、用两个栈实现一个队列

2、包含main函数的栈

3、栈的压入弹出序列

五、树和二叉树

1、重建二叉树

2、树的子结构

3、二叉树镜像

4、从上往下打印二叉树

5、二叉搜索树的后序遍历序列

6、二叉树中和为某一值的路径

7、二叉搜索树与双向链表

8、二叉树深度

9、平衡二叉树

10、二叉树的下一个结点

11、对称的二叉树

12、按之字形顺序打印二叉树

13、序列化二叉树

14、把二叉树打印成多行

15、二叉搜索树的第K个节点

六、动态规划

1、斐波那契数列

2、跳台阶

3、变态跳台阶

4、矩形覆盖

5、剪绳子

七、回溯法

1、矩阵中的路径

2、机器人运动范围

八、数学

1、数值的整数次方

2、求1+2+3+……+n

3、不用加减乘除做加法

 4、二进制中1的个数

九、其他

1、最小的K个数

2、从1到n整数中1出现的次数

3、丑数

4、和为S的连续正数序列

5、和为S的两个数字

6、扑克牌顺子

7、圆圈中最后剩下的数

8、数据流中的中位数

9、滑动窗口的最大值


一、数组

1、数组中重复的数字

 题目:

找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:

输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3 

LeetCode链接:力扣

思路:

  • 定义一个容器vector,以nums内元素作为索引,该元素重复个数做为该索引值
  • 遍历nums数组,根据vector说明,存入数据
  • 遍历vector,值大于1,返回该索引

示例代码:

 int findRepeatNumber(vector<int>& nums) {
        vector<int> vecTemp(nums.size(),0);
        for (auto &num : nums)
        {
            vecTemp[num]++;
        }
        for(auto i = 0; i < vecTemp.size() ;i++)
        {
            if (vecTemp[i] > 1)
            {
                return i;
            }
        }
        return -1;
    }

2、二维数组的查找

 题目:

在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

示例:

现有矩阵 matrix 如下:

[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]
给定 target = 5,返回 true。

给定 target = 20,返回 false。

LeetCode链接:力扣

思路:

三种方法

  • (1)直接遍历
  • (2)针对1优化,当元素大于目标元素,缩小列
  • (3)当元素小于目标元素,row++,当元素大于目标元素,col--

示例代码:

//直接遍历
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
    if (matrix.size() == 0 || matrix[0].size() == 0)
    {
        return false;
    }
    int n = matrix.size();
    int m = matrix.at(0).size();
    for(int i = 0 ;i < n;i++)
    {
        for (int j = 0;j < m;j++)
        {
            if (matrix[i][j] == target)
            {
                return true;
            }
        }
    }

    return false;
}

//遍历优化
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
    if (matrix.size() == 0 || matrix[0].size() == 0)
    {
        return false;
    }
    int n = matrix.size();
    int m = matrix.at(0).size();
    for(int i = 0 ;i < n;i++)
    {
        for (int j = 0;j < m;j++)
        {
            if (matrix[i][j] == target)
            {
                return true;
            }
            if (matrix[i][j] > target)
            {
                m = j;//将m-j后面的列去掉
            }
        }
    }

    return false;
}
//大于目标cal--,小于row++
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
    if (matrix.size() == 0 || matrix[0].size() == 0)
    {
        return false;
    }
    int n = 0;
    int m = matrix.at(0).size()-1;
    while(n < matrix.size() && m >= 0)
    {
        if (matrix[n][m] == target)
        {
            return true;
        }
        else if (matrix[n][m] > target)
        {
            m--;
        }
        else if (matrix[n][m] < target)
        {
            n++;
        }
    }
    return false;
}

3、数字在排序数组中出现的次数

 题目:

统计一个数字在排序数组中出现的次数。

示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
示例 2:

输入: nums = [5,7,7,8,8,10], target = 6
输出: 0

LeetCode链接:力扣

思路:

两种方法:

  • (1)直接遍历
  • (2)二分查找

示例代码:

//直接遍历
int search(vector<int>& nums, int target) {
    int nCount = 0;
    for (auto &num : nums)
    {
        if (num == target)
        {
            nCount++;
        }
    }
    return nCount;

}
//二分查找
 int search(vector<int>& nums, int target) {
    int nRet = 0;
    int left = 0;
    int right = nums.size()-1;
    while (left <= right)
    {
        int mid = left + (right - left)/2;
        if (nums[mid] == target)//找到
        {
            nRet++;
            int nIndex = mid-1;
            while(nIndex >= 0 &&nums[nIndex] == target )//向左查找
            {
                nRet++;
                nIndex--;
            }
            nIndex = mid + 1;
            while(nIndex < nums.size() && nums[nIndex] == target)//向右查找
            {
                nRet++;
                nIndex++;
            }  
            break; 
        }

        else if (nums[mid] > target)
        {
            right = mid-1;
        }
        else
        {
            left = mid+1;
        }
        
    }
    return nRet;
}

4、旋转数组的最小数字

 题目:

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。  

示例 1:

输入:[3,4,5,1,2]
输出:1
示例 2:

输入:[2,2,2,0,1]
输出:0

Leecode链接:力扣

思路:

两种方式:

  • (1)利用二分查找
  • (2)直接寻找,下一个元素比本身小,返回下一个元素

示例代码:

//二分法
   int minArray(vector<int>& numbers) {
        if (numbers.size() == 0)
        {
            return -65535;
        }

        int nLeft = 0;
        int nRight = numbers.size()-1;
        while(nLeft < nRight)
        {
            int nMid = nLeft + (nRight - nLeft)/2;
            if ( numbers[nMid] < numbers[nRight])//右侧有序,缩小区间
            {
                nRight = nMid;
            }
            else if (numbers[nMid] == numbers[nRight])//特殊情况,一个一个比对
            {
                nRight--;
            }
            else //左侧有序,缩小区间
            {
                nLeft = nMid+1;
            }
            //std::cout << nLeft << nRight << nMid <<std::endl;
        }
        return numbers[nLeft];
    }
//直接遍历
 int minArray(vector<int>& numbers) {
        if (numbers.size() == 0)
        {
            return -65535;
        }

        for(int i = 0; i < numbers.size()-1; ++i){
            if( numbers[i] > numbers[i + 1] ) 
            {
                return numbers[i+1];
            }
        }
        return numbers[nLeft];
    }

5、调整数组顺序使奇数位于偶数前面

 题目:

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数在数组的前半部分,所有偶数在数组的后半部分。

示例:

输入:nums = [1,2,3,4]
输出:[1,3,2,4] 
注:[3,1,2,4] 也是正确的答案之一。

Leecode链接:力扣

思路:

  • (双指针)定义两个变量left和right,分别指到数组头和尾
  • 遍历数组,为奇数移动left++后移,为偶数,交换left和right位置元素,right--前移

示例代码

 vector<int> exchange(vector<int>& nums) {
        int left = 0;
        int right = nums.size() - 1;
        while(left <= right)
        {
            if (nums[left]%2==1)
            {
                left++;
            }
            else
            {
                int nTemp = nums[right];
                nums[right] = nums[left];
                nums[left] = nTemp;
                right--;
            }
        }
        return nums;
    }

6、顺指针打印矩阵

题目:

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。

示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
示例 2:

输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]

Leecode链接:力扣

思路:

  • 先从左到右访问,行++
  • 再从上到下访问,总列数--
  • 从右到左访问,总行--
  • 从下到上访问,列++

示例代码:

vector<int> spiralOrder(vector<vector<int>>& matrix) {
    if (matrix.size() == 0)
    {
        return vector<int> ();
    }
    vector<int> ret;
    int top = 0;
    int bottom = 0;
    int nRow = matrix.size()-1;
    int nCol = matrix.at(0).size()-1;
    while (top <= nRow && bottom <= nCol)
    {
        for (int i = bottom; i < nCol;i++)//从左到右
        {
            ret.push_back(matrix[top][i]);
        }
        if (++top > nRow)
        {
            break;
        }
        for (int j = top; j < nRow;j++)//从上到下
        {
            ret.push_back(matrix[j][nCol]);
        }
        if (--nCol < bottom)
        {
            break;
        }

        for (int k = nCol; k >= bottom;k--)//从右到左
        {
            ret.push_back(matrix[nRow][k]);
        }
        if (--nRow < top)
        {
            break;
        }

        for (int m = nRow; m >= top;m--)//从下到上
        {
            ret.push_back(matrix[m][bottom]);
        }
        if (++bottom > nCol)
        {
            break;
        }
    }
    return ret;
}

7、数组中出现次数超过一半的数字

 题目:

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2

LeetCode链接:力扣

思路:

两种方式:

  • (1)先排序,取中间值
  • (2)摩尔投票法:相同++,不同--,最后剩下的元素即是

示例代码:

//排序,取中
    int majorityElement(vector<int>& nums) {
       std::sort(nums.begin(),nums.end());
        return nums[nums.size()/2];
    }
//摩尔投票法
    int majorityElement(vector<int>& nums) {
        int nCount = 0;
        int nTemp = 0;
        for (auto &val : nums)
        {
            if (nCount == 0)
            {
                nTemp = val;
                nCount++;
            }
            else
            {
                nTemp == val ? nCount++ : nCount--;
            }
        }
        return nTemp;
    }

8、连续子数组的最大和

题目:

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。

示例1:

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

LeetCode链接:力扣

思路:

两种方法:

  • (1)直接在数组中修改(之前最大值+当前值>当前值则最大值 = 之前最大值+当前值,否则最大值 = 当前值)
  • (2)通过dp思想

示例代码:

//原数组修改
int maxSubArray(vector<int>& nums) {
    if (nums.size() == 0)
    {
        return 0;
    }
    int nMax = nums[0];
    int result = nMax;
    for (int i = 1; i < nums.size(); i++)
    {
        if (nMax + nums[i] > nums[i])
        {
            nMax = nMax + nums[i];
        }
        else
        {
            nMax = nums[i];
        }
        result = max(nMax, result);
    }
    return  result;
}
//dp思想
int maxSubArray(vector<int>& nums) {
    if (nums.size() == 0)
    {
        return 0;
    }
    int nMax = nums[0];
    vector<int> dp(nums.size(),0);
    dp[0] = nMax;
    
    for (int i = 1; i < nums.size(); i++)
    {
        dp[i] = max(nums[i],nums[i] + dp[i-1]);
        nMax = max(nMax,dp[i]);
    }
    return  nMax;
}

9、数组中的逆数对

题目:

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:

输入: [7,5,6,4]
输出: 5

LeetCode链接:力扣

思想:

  • 采用归并排序思想
  • 每当遇到 左子数组当前元素 > 右子数组当前元素 时,意味着 「左子数组当前元素 至 末尾元素」 与 「右子数组当前元素」 构成了若干 「逆序对」

如图:

Picture1.png

 示例代码:

int reversePairs(vector<int>& nums) {
    vector<int> tmp(nums.size());
    return mergeSort(0, nums.size() - 1, nums, tmp);
}
int mergeSort(int l, int r, vector<int>& nums, vector<int>& tmp) {
    // 终止条件
    if (l >= r) return 0;
    // 递归划分
    int m = (l + r) / 2;
    int res = mergeSort(l, m, nums, tmp) + mergeSort(m + 1, r, nums, tmp);
    // 合并阶段
    int i = l, j = m + 1;
    for (int k = l; k <= r; k++)
        tmp[k] = nums[k];
    for (int k = l; k <= r; k++) {
        if (i == m + 1)
            nums[k] = tmp[j++];
        else if (j == r + 1 || tmp[i] <= tmp[j])
            nums[k] = tmp[i++];
        else {
            nums[k] = tmp[j++];
            res += m - i + 1; // 统计逆序对
        }
    }
    return res;
}

10、数组中只出现过一次的数字

题目:

一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字

Leecode链接:力扣

思路:

三种方法:

  • (1)排序,遍历
  • (2)unordered_map遍历存储
  • (3)异或(每个数异或,得到一个结果ret,ret一定是所求数字异或结果。寻找ret二级制中1的位置。如何找到位置i?可用i = ret ^ (-ret)因为计算机用补码存取二进制数,而负数的补码为反码+1,举个例子:假如ret = 1110 , -ret = 0010 , 所以 i = 0010所以,再异或一遍即可得到答案)

示例代码:

//排序,遍历
 void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
         std::sort(data.begin(),data.end());
        vector<int> tempVec;
        for (int i = 0;i < data.size();i++)
        {
                if (i+1 < data.size() && data[i] == data[i+1])
                {
                    i++;
                    continue;
                }
                tempVec.push_back(data[i]);
        }
        *num1 = tempVec[0];
        *num2 = tempVec[1];
    }
//哈希表,遍历
 void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
        unordered_map<int, int> mp;
        for (const int k : data) ++mp[k];
        vector<int> ret;
        for (const int k : data) {
            if (mp[k] == 1) {
                ret.push_back(k);
            }
        }
        *num1 = ret[0];
        *num2 = ret[1];
    }
//异或
 void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
        int ret = 0;
        for (const int k : data) ret ^= k;
        ret &= (-ret);
        *num1 = 0, *num2 = 0;
        for (const int k : data) {
            if (k & ret) *num1 ^= k;
            else *num2 ^= k;
        }
    }

11、把数组排成最小的数

题目:

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

示例 1:

输入: [10,2]
输出: "102"
示例 2:

输入: [3,30,34,5,9]
输出: "3033459"

LeetCode链接:力扣

思路:

  • 高位越小越好
  • 例如:1和20,组成120
  • 放数字先放第一位最小的元素,如果第一位相等,在比较第二位

示例代码:

string minNumber(vector<int>& nums) {
    vector<string>vecRet;
    string ans;
    for(int i = 0; i < nums.size(); i ++){
        vecRet.push_back(to_string(nums[i]));
    }
    sort(vecRet.begin(), vecRet.end(), [](string& s1, string& s2){return s1 + s2 < s2 + s1;});
    for(int i = 0; i < vecRet.size(); i ++)
    {
        ans += vecRet[i];

    }
    return ans;
}

12、构建乘积数组

题目:

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

示例:

输入: [1,2,3,4,5]
输出: [120,60,40,30,24]

LeetCode链接:力扣

思想:

两种方法:

  • (1)暴力遍历
  • (2)上下三角

Picture1.png

 示例代码:

//暴力遍历
vector<int> constructArr(vector<int>& a) {
    if (a.size() == 0)
    {
        return vector<int>();
    }
    vector<int> ret;

    for (int i = 0 ;i < a.size();i++)
    {
        int temp = 1;
        for (int j = 0; j < a.size();j++)
        {
            if (i == j)
            {
                continue;
            }
            temp*=a[j];
        }
        ret.push_back(temp);
    }

    return ret;
}
//上下三角
vector<int> constructArr(vector<int>& a) {
    vector<int> ret(a.size());
    if(a.size() <= 0){
        return ret;
    }
    ret[0] = 1;
    for(int i = 1; i < a.size(); i++){
        ret[i] = ret[i - 1] * a[i - 1];
    }
    int temp = 1;
    for(int i = a.size() - 2; i >= 0; i--){
        temp *= a[i + 1];
        ret[i] *= temp;
    }
    return ret;
}

二、链表

1、从尾到头打印链表

 题目:

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

示例 1:

输入:head = [1,3,2]
输出:[2,3,1]

LeetCode链接:力扣

思路:

  • 定义一个容器vector
  • 遍历链表,将值存入容器
  • 反向输出vector

示例代码:

 vector<int> reversePrint(ListNode* head) {
        vector<int> vecTemp;
        while (head != nullptr)
        {
            vecTemp.push_back(head->val);
            head = head->next;
        }

        return vector<int>(vecTemp.rbegin(),vecTemp.rend());
    }

2、链表中倒数第K个节点

 题目:

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

示例:

给定一个链表: 1->2->3->4->5, 和 k = 2.

返回链表 4->5.

LeetCode链接:力扣

思路:

  • 采用快慢指针,定义两个指针fast,slow同时指向链表头
  • 快指针先走k步
  • 快指针和慢指针同时走,当快指针到达链表尾,慢指针即倒数k节点

示例代码:

ListNode* getKthFromEnd(ListNode* head, int k) {
        if (head == nullptr || k < 0)
        {
            return head;
        }
        ListNode *fast= head;
        ListNode *slow = head;
        while(k--> 1)
        {
            fast = fast->next;
        }
        while(fast != nullptr && fast->next != nullptr)
        {
            slow = slow->next;
            fast = fast->next;
        }

        return slow;
    }

3、反转链表

 题目:

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:


输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:


输入:head = [1,2]
输出:[2,1]
示例 3: 

输入:head = []
输出:[]

LeetCode链接:力扣

代码示例:

//就地反转
ListNode * reverse1(ListNode *node)
{
    if (node == nullptr)
    {
        return node;
    }
    ListNode *head = new ListNode(-1);
    head->next = node;
    ListNode *curNode = node;
    ListNode *nextNode = curNode->next;
    while (nextNode != nullptr) {
        curNode->next = nextNode->next;
        nextNode->next = head->next;
        head->next = nextNode;
        nextNode = curNode->next;
    }
    return head->next;
}
//头插法
ListNode * reverse2(ListNode *node)
{
    ListNode *preNode = new ListNode(-1);
    ListNode *pCur = node;
    while (pCur != nullptr) {
        ListNode *pNext = pCur->next;
        pCur->next = preNode->next;
        preNode->next = pCur;
        pCur = pNext;
    }
    return preNode->next;
}
//迭代
ListNode * reverse3(ListNode *node)
{
    ListNode *prev = nullptr;
    while(node != nullptr){
        ListNode *tmpNode = node->next;
        node->next = prev;
        prev = node;
        node = tmpNode;
    }
    return prev;
}
//递归
ListNode * reverse4(ListNode *node)
{
    if(node == nullptr|| node->next == nullptr)
    {
        return node;
    }
    ListNode *prev = reverse4(node->next);
    node->next->next = node;
    node->next = nullptr;
    return prev;
}
//创建初始化链表
ListNode * createListNode()
{
    ListNode *head = nullptr;
    ListNode *node1 = new  ListNode(1);
    ListNode *node2 = new  ListNode(2);
    ListNode *node3 = new  ListNode(3);
    ListNode *node4 = new  ListNode(4);
    ListNode *node5 = new  ListNode(5);
    head = node1;
    node1->next = node2;
    node2->next = node3;
    node3->next = node4;
    node4->next = node5;
    node5->next = nullptr;
    ListNode *newHead = reverse(head);
    while (newHead != nullptr)
    {
        qDebug() << newHead->val;
        newHead = newHead->next;
    }
}

4、合并两个排序链表

 题目:将两个有序链表合并成一个新的链表,新链表为通过两个链表拼接成新的节点生成

输入:1->2->4,1->3->4

输出:1->1->2->3->4->4

LeetCode链接:力扣

分析:

1)如果有一个链表为空,返回另外一个链表即可

2)遍历两个链表,比较值,取出较小值地址赋给新链表,则原链表后移(list->next=list)

3)其中一个链表遍历完,另外一个链表可能还存在多余的节点,直接赋值给新链表

代码示例:

struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* listNode1, ListNode* listNode2) {
        if (listNode1 == nullptr)//如果链表1为空,直接返回链表2
        {
            return listNode2;
        }
        if (listNode2 == nullptr)//如果链表2为空,直接返回链表1
        {
            return listNode1;
        }
        ListNode head(0);
        ListNode* node = &head;
        while(listNode1 != nullptr && listNode2 != nullptr)
        {
            if (listNode1->val > listNode2->val)//如果链表1节点大于链表2节点,将链表2节点地址赋给node,链表2后移
            {
                node->next = listNode2;
                listNode2 = listNode2->next;
            }
            else //如果链表1节点小于链表2节点,将链表1节点地址赋给node,链表1后移
            {
                node->next = listNode1;
                listNode1= listNode1->next;
            }
            node = node->next;
        }

        if (listNode1 == nullptr) //链表1遍历完,将剩余链表2赋值给node
        {
            node->next = listNode2;
        }

        if (listNode2 == nullptr)//链表2遍历完,将剩余链表1赋值给node
        {
            node->next = listNode1;
        }

        return head.next;
    }
};

5、两个链表的第一个公共节点

 题目:

输入两个链表,找出它们的第一个公共节点。

如下面的两个链表:

在节点 c1 开始相交。

示例 1:

 输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例 2:

输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
输入解释:相交节点的值为 2 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

 输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。

LeetCode链接:力扣

思路:

  • 定义两个节点tempA指向headA,tempB指向headB
  • 同时移动tempA和tempB,当其中一个节点到末尾,将该节点指向另外一个链表头部(节点个数差)

示例代码:

   ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if (headA == nullptr || headB == nullptr)
        {
            return nullptr;
        }
        ListNode *tempA = headA;
        ListNode *tempB = headB;
        while(tempA != tempB)
        {
            tempA = tempA == nullptr ? headB:tempA->next;
            tempB = tempB == nullptr ? headA:tempB->next;
        }
        return tempA;
    }

6、链表中环的入口节点

题目:

给定一个链表,返回链表开始入环的第一个节点。 从链表的头节点开始沿着 next 指针进入环的第一个节点为环的入口节点。如果链表无环,则返回 null。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

说明:不允许修改给定的链表。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

LeetCode链接:力扣

思路:

  • 定义快慢指针,如果快慢指针相等则有环
  • 把其中一个节点重新设置成头结点,再次相等位置则为环入口

示例代码:

ListNode *detectCycle(ListNode *head) {
    if (head == nullptr)
    {
        return head;
    }
    
    ListNode *fast = head;
    ListNode *slow = head;
    
    bool bRet = false;
    while(fast != nullptr && fast->next != nullptr)
    {
        fast = fast->next->next;
        slow = slow->next;
        
        if (slow == fast)
        {
            bRet = true;
            break;
        }
    }
    if (!bRet)
    {
        return nullptr;
    }
    slow = head;
    while(slow != fast)
    {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;
}

7、删除链表中重复的节点

题目:

存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中 没有重复出现 的数字。

返回同样按升序排列的结果链表

示例 1:


输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
示例 2:


输入:head = [1,1,1,2,3]
输出:[2,3]

LeetCode链接:力扣

思路:

  • 三指针法
  • 添加一个头结点
  • 设置pre、cur指针,pre指针指向当前确定不重复的节点,cur指针是工作指针,一直向后搜索

示例代码:

ListNode* deleteDuplication(ListNode* pHead)
{
    
    if(pHead == nullptr || pHead->next == nullptr) return pHead;
    ListNode dummpyHead(0);
    dummpyHead.next = pHead;
    ListNode *pre = &dummpyHead;
    ListNode *cur = dummpyHead.next;//cur是真正工作的节点
    while(cur != nullptr){
        if(cur->next != nullptr && cur->val == cur->next->val){
            while(cur->next != nullptr && cur->val == cur->next->val)
            {
                cur = cur->next;
            }
            pre->next = cur->next;//这里还不要马上把 pre 赋值过来
            cur = cur->next;
        }else{
            pre = pre->next;
            cur = cur->next;
        }
    }
    return dummpyHead.next;
}

8、复杂链表的复制

题目:

请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。

示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:

输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
示例 3:

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]
示例 4:

输入:head = []
输出:[]
解释:给定的链表为空(空指针),因此返回 null。

LeetCode链接:力扣
 

Picture1.png

 思路:

  • 始化: 哈希表 dic , 节点 cur 指向头节点
  • 复制链表:建立新节点,并向 dic 添加键值对 (原 cur 节点, 新 cur 节点)
  • 构建新链表的引用指向

示例代码:

Node* copyRandomList(Node* head) {
    if(head == nullptr) return nullptr;
    Node* cur = head;
    unordered_map<Node*, Node*> map;
    // 3. 复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射
    while(cur != nullptr) {
        map[cur] = new Node(cur->val);
        cur = cur->next;
    }
    cur = head;
    // 4. 构建新链表的 next 和 random 指向
    while(cur != nullptr) {
        map[cur]->next = map[cur->next];
        map[cur]->random = map[cur->random];
        cur = cur->next;
    }
    // 5. 返回新链表的头节点
    return map[head];
}

三、字符串

1、替换空格

 题目:请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

示例 1:

输入:s = "We are happy."
输出:"We%20are%20happy."

LeetCode链接:力扣

思路:

  • 定义一个容器
  • 遍历字符串s,将s中非空格字符存入容器中
  • 遇到空格字符,替换成“20%”存入容器中

示例代码:

 string replaceSpace(string s) {
        string strRet = "";
        for(auto &c : s)
        {
            if (c == ' ')
            {
                strRet += "%20";
            }
            else {
                strRet.push_back(c);
            }
        }
        return strRet;
    }

2、字符串的排列

题目:

输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

示例:

输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]

LeetCode链接:力扣

思路:

两种方法:

  • (1)调用c++函数next_permutation,字典序取得[first,last)所标示之序列的下一个排列组合
  • (2)回溯算法

示例代码:

//使用c++的next_permutation函数
vector<string> permutation(string s) {
    sort(s.begin(), s.end());
    vector<string> res;
    do{
        res.emplace_back(s);
    }while(next_permutation(s.begin(), s.end()));

    return res;
}
//回溯算法
std::set<string> setRet;
vector<string> vecRet;

void dfs(string& s, string& cur, vector<bool>& used){
    if(cur.size() == s.size()){
        setRet.insert(cur);
        return;
    }
    for(int i = 0; i < s.size(); ++i){
        if(used[i]) continue;
        cur += s[i];
        used[i] = true;
        dfs(s, cur, used);
        used[i] = false;
        cur.pop_back();
    }
}

vector<string> permutation(string s) {
    string cur = "";
    vector<bool> used(s.size(), false);
    dfs(s, cur, used);

    for(auto& ele : setRet) vecRet.push_back(ele);
    return vecRet;
}

3、第一个只出现一次的字符

 题目:

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

示例 1:

输入:s = "abaccdeff"
输出:'b'
示例 2:

输入:s = "" 
输出:' '

LeetCode链接:力扣

思路:

  • 定义unordered_map<char,int>map
  • 反方向遍历字符串s,存储到map
  • 遍历map,value==1.返回key值

示例代码:

  char firstUniqChar(string s) {
        unordered_map<char,int> map;
        for (int i = s.size()-1;i >= 0 ;i--)
        {
            map[s[i]]++;
        }
        
        for (auto & val : map)
        {
            std::cout << val.second << val.first << std::endl;
            if (val.second == 1)
            {
                return val.first;
            }
        }
        return ' ';
    }

4、左旋转字符串

题目:

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

示例 1:

输入: s = "abcdefg", k = 2
输出: "cdefgab"
示例 2:

输入: s = "lrloseumgh", k = 6
输出: "umghlrlose"

LeetCode链接:力扣

思路:

  • 如果循环次数大于字符串长度,取模
  • 字符串+字符串,截取

示例代码:

string reverseLeftWords(string s, int n) {
    if (n <= 0)
    {
        return s;
    }
    int nLen = s.length();
    if (n >= nLen)
    {
        n = n%nLen;
    }
    s+=s;
    return s.substr(n,nLen);
}

5、反转单词序列

题目:

输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. ",则输出"student. a am I"。

示例 1:

输入: "the sky is blue"
输出: "blue is sky the"
示例 2:

输入: "  hello world!  "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
示例 3:

输入: "a good   example"
输出: "example good a"
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

LeetCode链接:力扣

思路:

两种方法:

  • (1)直接遍历(strTemp用于存储单词,需要处理收尾空格,和多个空格问题)
  • (2)双指针

示例代码:

//直接遍历
string reverseWords(string s) {
    if (s.length() == 0 )
    {
        return s;
    }
    
    string strRet = "";
    string strTemp = "";
    for (auto &ch : s)
    {
        if (ch != ' ')
        {
            strTemp += ch; 
        }
        else if (strTemp.length() != 0)//处理多个空格问题
        {
            strRet =" " + strTemp + strRet;
            strTemp = "";
        }
    }
    if (strTemp.size() != 0) 
    {
        strRet = strTemp + strRet;
    }
    if (strRet[0] == ' ')//处理收尾空格
    {
        strRet = strRet.substr(1,strRet.length()-1);
    }
    
    return strRet;
}
//双指针
string reverseWords(string s) {
    //nBegin,nEnd用于确定跳过首尾空格的下标范围
    int nBegin = 0, nEnd = s.length()-1;
    //跳过s的首部空格
    while(nBegin <= nEnd&&s[nBegin]==' ') nBegin++;
    //跳过s的尾部空格
    while(nBegin <= nEnd&&s[nEnd]==' ') nEnd--;

    //k,w为用于确定每个单词范围的双指针,从非空格尾部开始往前扫描,nBegin为前边界,nEnd为后边界
    int k = nEnd, w = nEnd;
    //当输入全为空格时,跳过首尾空格后nBegin > nEnd
    string strRet;
    while(nBegin <= nEnd && k >= nBegin){
        //k往前扫描直到遇到空格停下,或者超出nBegin前边界停下
        while(k >= nBegin && s[k] !=' ') k--;
        //k+1到w为一个单词的范围,将每个字符按序加入string strRet即可
        for(int i = k+1;i <= w; i++) strRet += s[i];
        //没超出前边界nBegin时,k停下遇到的肯定是空格,可能是一个或多个,跳过
        if(k >= nBegin && s[k] ==' '){
            while(k >= nBegin && s[k] ==' ') k--;
            //跳过一个或多个空格后,strRet加一个必要的空格
            strRet += ' ';
        }
        //w跳到k位置继续扫描下一个单词范围
        w = k;
    }
    return strRet;
}

6、把字符串转换成整数

题目:

写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。

首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。

当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。

该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。

注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。

在任何情况下,若函数不能进行有效的转换时,请返回 0。

说明:

假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231,  231 − 1]。如果数值超过这个范围,请返回  INT_MAX (231 − 1) 或 INT_MIN (−231) 。

示例 1:

输入: "42"
输出: 42
示例 2:

输入: "   -42"
输出: -42
解释: 第一个非空白字符为 '-', 它是一个负号。
     我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。
示例 3:

输入: "4193 with words"
输出: 4193
解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。
示例 4:

输入: "words and 987"
输出: 0
解释: 第一个非空字符是 'w', 但它不是数字或正、负号。
     因此无法执行有效的转换。
示例 5:

输入: "-91283472332"
输出: -2147483648
解释: 数字 "-91283472332" 超过 32 位有符号整数范围。 
     因此返回 INT_MIN (−231) 。

LeetCode链接:力扣

思路:

  • 去除头部空格
  • 判断是否有正负值符号
  • 不符合条件直接返回0
  • 遍历,累加
  • 不符合条件返回0,超过最大值或最小值,返回

示例代码:

int strToInt(string str) {
    if (str.size() == 0 || (str[0] >= 'a' && str[0] <= 'z'))//空字符、首字符不符合
    {
        return 0;
    }
    int nBegin = 0;
    while (nBegin <= str.size()-1 && str[nBegin] == ' ')nBegin++;//去除前面空格
    if (nBegin >= str.size())//如果全是空格串
    {
        return 0;
    }
    int nSign = 1;
    if (str[nBegin] == '-')//判断正负号
    {
        nSign = -1;
        nBegin++;
    }
    else if (str[nBegin] == '+')
    {
        nSign = 1;
        nBegin++;
    }
    long nRet = 0;
    for (int  i = nBegin; i < str.size(); i++)
    {
        if (str[i] < '0' || str[i] > '9')//不符合退出
        {
            break;
        }
        nRet = nRet *10 + nSign *(str[i] - '0');//累加
        if (nRet > INT_MAX)//大于最大值
        {
            return INT_MAX;
        }
        if (nRet < INT_MIN)//小于最小值
        {
            return INT_MIN;
        }
    }
    return nRet;
}

7、正则表达式匹配

题目:

请实现一个函数用来匹配包含'. '和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但与"aa.a"和"ab*a"均不匹配。

示例 1:

输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:

输入:
s = "aa"
p = "a*"
输出: true
解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3:

输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
示例 4:

输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
示例 5:

输入:
s = "mississippi"
p = "mis*is*p*."
输出: false

LeetCode链接:力扣

思路:

  • 特判,同时也是递归出口,如果p是空串,返回s是否为空串。如果p不为空,保证一定存在p[1](可能是字符串结尾\0)
  • 假如p[1] == * 的话,可以尝试两种情况:情况一是递归比较s和p.substr(2);情况二是当s[0]可以匹配p[0]时, 尝试递归比较s.substr(1)和p,这里没有必要比较s.substr(1) 和 p.substr(2),因为这种情况已经包含在递归比较s.substr(1)和p当中了
  • 假如p[1] != *,如果p[0]不匹配s[0],返回false,否则递归判断s.substr(1)和p.substr(1)

示例代码:

bool isMatch(string s, string p) {
    int m = s.size();
    int n = p.size();
    std::vector<std::vector<bool>> dp (m + 1,vector<bool>(n + 1,false));
    for(int i = 0;i <= m; i++){
        for(int j = 0; j <= n; j++){
            if(j == 0)//正则串是空串的dp初始化
            {
                dp[i][j] = i == 0; //如果i==0为true,否则为false
            }
            else if(p[j-1] != '*')
            {
                if(i > 0 && (s[i-1] == p[j-1] || p[j-1] =='.'))
                {
                    dp[i][j] = dp[i-1][j-1];
                }
            }
            else
            {
                if(j > 1)//不看,直接砍掉p后两个字符
                {
                    dp[i][j] = dp[i][j] | dp[i][j-2];
                }
                if(i > 0 && j > 1 && (s[i-1] == p[j-2] || p[j-2] == '.'))
                {
                    dp[i][j] = dp[i][j] | dp[i-1][j];
                }
            }
        }
    }
    return dp[m][n];
}

8、表示数值的字符串

题目:

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。

数值(按顺序)可以分成以下几个部分:

若干空格
一个 小数 或者 整数
(可选)一个 'e' 或 'E' ,后面跟着一个 整数
若干空格
小数(按顺序)可以分成以下几个部分:

(可选)一个符号字符('+' 或 '-')
下述格式之一:
至少一位数字,后面跟着一个点 '.'
至少一位数字,后面跟着一个点 '.' ,后面再跟着至少一位数字
一个点 '.' ,后面跟着至少一位数字
整数(按顺序)可以分成以下几个部分:

(可选)一个符号字符('+' 或 '-')
至少一位数字
部分数值列举如下:

["+100", "5e2", "-123", "3.1416", "-1E-16", "0123"]
部分非数值列举如下:

["12e", "1a3.14", "1.2.3", "+-5", "12e+5.4"]

示例 1:

输入:s = "0"
输出:true
示例 2:

输入:s = "e"
输出:false
示例 3:

输入:s = "."
输出:false
示例 4:

输入:s = "    .1  "
输出:true

LeetCode链接:力扣

思路:

  • 先去除字符串首尾的空格
  • 然后根据e划分指数和底数
  • 判断指数和底数是否合法即可

示例代码:

bool isNumber(string s) {
    //1、从首尾寻找s中不为空格首尾位置,也就是去除首尾空格
    int i=s.find_first_not_of(' ');
    if(i==string::npos)return false;
    int j=s.find_last_not_of(' ');
    s=s.substr(i,j-i+1);
    if(s.empty())return false;

    //2、根据e来划分底数和指数
    int e=s.find('e');

    //3、指数为空,判断底数
    if(e==string::npos)return judgeP(s);

    //4、指数不为空,判断底数和指数
    else return judgeP(s.substr(0,e))&&judgeS(s.substr(e+1));
}

bool judgeP(string s)//判断底数是否合法
{
    bool result=false,point=false;
    int n=s.size();
    for(int i=0;i<n;++i)
    {
        if(s[i]=='+'||s[i]=='-'){//符号位不在第一位,返回false
            if(i!=0)return false;
        }
        else if(s[i]=='.'){
            if(point)return false;//有多个小数点,返回false
            point=true;
        }
        else if(s[i]<'0'||s[i]>'9'){//非纯数字,返回false
            return false;
        }
        else{
            result=true;
        }
    }
    return result;
}

bool judgeS(string s)//判断指数是否合法
{
    bool result=false;
    //注意指数不能出现小数点,所以出现除符号位的非纯数字表示指数不合法
    for(int i=0;i<s.size();++i)
    {
        if(s[i]=='+'||s[i]=='-'){//符号位不在第一位,返回false
            if(i!=0)return false;
        }
        else if(s[i]<'0'||s[i]>'9'){//非纯数字,返回false
            return false;
        }
        else{
            result=true;
        }
    }
    return result;
}

9、字符流中第一个不重复的字符

题目:

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

示例 1:

输入:s = "abaccdeff"
输出:'b'
示例 2:

输入:s = "" 
输出:' '

LeetCode链接:力扣

思路:

  • 借助hash存储
  • 遍历hash

示例代码:

char firstUniqChar(string s) {
    unordered_map<char,int> map;
    for (int i = s.size()-1;i >= 0 ;i--)
    {
        map[s[i]]++;
    }

    for (auto & val : map)
    {
        std::cout << val.second << val.first << std::endl;
        if (val.second == 1)
        {
            return val.first;
        }
    }
    return ' ';
}

四、栈和队列

1、用两个栈实现一个队列

 题目:

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

示例 1:

输入:
["CQueue","appendTail","deleteHead","deleteHead"]
[[],[3],[],[]]
输出:[null,null,3,-1]
示例 2:

输入:
["CQueue","deleteHead","appendTail","appendTail","deleteHead","deleteHead"]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]

LeetCode链接:力扣

思路:

  • 定义两个栈,一个用于放入数据,一个用于取数据
  • 入队列操作,即为出入stack1
  • 出队列,判断stack2不空,弹出栈顶元素,若空,将stack1依次将元素出栈,存入stack2中

示例代码:

class CQueue {
public:
    CQueue() {

    }
    
    void appendTail(int value) {
        m_stack1.push(value);
    }
    
    int deleteHead() {
        if (m_stack2.empty())
        {
            while(!m_stack1.empty())
            {
                m_stack2.push(m_stack1.top());
                m_stack1.pop();
            }
        }
        int n = -1;
        if (!m_stack2.empty())
        {
            n = m_stack2.top();
            m_stack2.pop();
        }
        return n;
    }

    std::stack<int> m_stack1;
    std::stack<int> m_stack2;
};

2、包含main函数的栈

题目:

定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。

示例:

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.min();   --> 返回 -2.

LeetCode链接:力扣

思路:

  • 用vector保存数据

示例代码:

    MinStack() {
        m_vec.clear();
    }
    
    void push(int x) {
        m_vec.push_back(x);
    }
    
    void pop() {
        m_vec.pop_back();
    }
    
    int top() {
        return m_vec[m_vec.size()-1];
    }
    
    int min() {
        int nMin = INT_MAX;
        for (auto &n : m_vec)
        {
            nMin = n > nMin ? nMin : n;
        }
        return nMin;
    }
    vector<int> m_vec;
};

3、栈的压入弹出序列

题目:

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。

示例 1:

输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
示例 2:

输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
输出:false
解释:1 不能在 2 之前弹出。

LeetCode链接:力扣

思路:

  • 使用栈,按照顺序入栈
  • 同时判断栈顶和出栈顺序相同,则出栈,直到栈空或者栈顶元素和出栈顺序不同
  • 返回栈是否为空

示例代码:

bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
    stack<int> stk;
    int nIndex = 0;
    for(auto &val : pushed)
    {
        stk.push(val);
        while (!stk.empty() && stk.top() == popped[nIndex])
        {
            stk.pop();  
            nIndex++;
        }
    }
    return stk.empty();
}

五、树和二叉树

1、重建二叉树

 题目:

输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。

假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

示例 1:


Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]
示例 2:

Input: preorder = [-1], inorder = [-1]
Output: [-1]

LeetCode链接:力扣

思路:

  • 前序遍历列表:第一个元素永远是 【根节点 (root)】
  • 中序遍历列表:根节点 (root)【左边】的所有元素都在根节点的【左分支】,【右边】的所有元素都在根节点的【右分支】
  • 通过【前序遍历列表】确定【根节点 (root)】
  • 将【中序遍历列表】的节点分割成【左分支节点】和【右分支节点】
  • 递归寻找【左分支节点】中的【根节点 (left child)】和 【右分支节点】中的【根节点 (right child)】

示例代码:

TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
    this->preorder = preorder;
    for(int i = 0; i < inorder.size(); i++)
        dic[inorder[i]] = i;
    return recur(0, 0, inorder.size() - 1);
}
vector<int> preorder;
unordered_map<int, int> dic;
TreeNode* recur(int root, int left, int right) {
    if(left > right) return nullptr;                        // 递归终止
    TreeNode* node = new TreeNode(preorder[root]);          // 建立根节点
    int i = dic[preorder[root]];                            // 划分根节点、左子树、右子树
    node->left = recur(root + 1, left, i - 1);              // 开启左子树递归
    node->right = recur(root + i - left + 1, i + 1, right); // 开启右子树递归
    return node;                                            // 回溯返回根节点
}

2、树的子结构

题目:

输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)

B是A的子结构, 即 A中有出现和B相同的结构和节点值。

例如:
给定的树 A:

     3
    / \
   4   5
  / \
 1   2
给定的树 B:

   4 
  /
 1
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。

示例 1:

输入:A = [1,2,3], B = [3,1]
输出:false
示例 2:

输入:A = [3,4,5,1,2], B = [4,1]
输出:true

LeetCode链接:力扣

思路:

  • A或者B为空,不是子树;
  • A和B值相等,继续查找;
  • A和B值不相等,则遍历A左子树和右子树。
  • B如果已经空,则匹配完,为子树:
  • 如果A已经空,或者A和B值不相等,则不是子树;
  • 递归比对A左节点和B左节点,A右节点和B右节点;

示例代码:

bool isSubStructure(TreeNode* A, TreeNode* B) {
    if (A == nullptr || B == nullptr)
    {
        return false;
    }
    bool bRet = false;
    if (A->val == B->val)//A、B相同,继续查找
    {
        bRet= helpCheck(A,B);
    }
    if (!bRet)
    {
        bRet = isSubStructure(A->left,B) || isSubStructure(A->right,B);//A和B不相同,遍历左子树和右子树
    }
    return bRet;
}
bool helpCheck(TreeNode* A, TreeNode* B)
{
    if (B == nullptr)//B遍历成功,是子树
    {
        return true;
    }
    if (A == nullptr || A->val != B->val)//A遍历完或者A不等于B值,不是子树
    {
        return false;
    }
    return helpCheck(A->left,B->left) && helpCheck(A->right,B->right);//递归遍历
}

3、二叉树镜像

 题目:

请完成一个函数,输入一个二叉树,该函数输出它的镜像。

例如输入:

     4
   /   \
  2     7
 / \   / \
1   3 6   9
镜像输出:

     4
   /   \
  7     2
 / \   / \
9   6 3   1

示例 1:

输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

LeetCode链接:力扣

思路:

两种方式

  • (1)递归:交换左右节点
  • (2)迭代:通过栈,交换左右节点

示例代码:

//递归
 TreeNode* mirrorTree(TreeNode* root) {
        if (root == nullptr)
        {
            return root;
        }
        TreeNode *tempNode = root->left;
        root->left = root->right;
        root->right = tempNode;
        mirrorTree(root->left);
        mirrorTree(root->right);

        return root;
    }
//迭代
 TreeNode* mirrorTree(TreeNode* root) {
        stack<TreeNode*> sck;
        sck.push(root);
        while(!sck.empty())
        {
            TreeNode* tmp = sck.top();
            sck.pop();
            if(!tmp) continue;
            TreeNode *tempNode = tmp->left;
            tmp->left = tmp->right;
            tmp->right = tempNode;
            if(tmp->right != nullptr) sck.push(tmp->right);
            if(tmp->left != nullptr) sck.push(tmp->left);
        }
        return root;
    }

4、从上往下打印二叉树

题目:

不分行从上往下打印出二叉树的每个节点,同层节点从左至右打印。例如输入{8,6,10,#,#,2,1},如以下图中的示例二叉树,则依次打印8,6,10,2,1(空节点不打印,跳过),请你将打印的结果存放到一个数组里面,返回。

数据范围:

0<=节点总数<=1000

-1000<=节点值<=1000

示例1

输入:{8,6,10,#,#,2,1}

返回值:[8,6,10,2,1]

示例2

输入:{5,4,#,3,#,2,#,1}

返回值:[5,4,3,2,1] 

LeetCode链接:力扣

思路:

  • dfs进行搜索

示例代码:

vector<int> levelOrder(TreeNode* root) {
    if (root == nullptr)
    {
        return vector<int>();
    }
    vector<int> ret; 
    queue<TreeNode*> q; // 队列模拟层次遍历
    q.push(root); // 根节点首先入队
    while (!q.empty()) {
        /*
            *  每次外层while循环开始
            *  队列中存储的节点都位于二叉树的同一层
            *  nCount记录此时队列的长度,也就是二叉树某一层的宽度
            */
        int nCount = q.size();
        while (nCount--) {
            /*
                *  内层while循环将队列中处在二叉树同一层的所有节点出队
                *  并将下一层的所有节点从左到右入队
                */
            TreeNode* node = q.front();
            ret.push_back(node->val);
            q.pop();
            if (node->left) q.push(node->left);
            if (node->right) q.push(node->right);
        }
    }
    return ret;
}

5、二叉搜索树的后序遍历序列

题目:

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。

参考以下这颗二叉搜索树:

     5
    / \
   2   6
  / \
 1   3
示例 1:

输入: [1,6,3,2,5]
输出: false
示例 2:

输入: [1,3,2,6,5]
输出: true

LeetCode链接:力扣

思路:

  • 按照后续遍历的实现最后一个元素是根节点, 比根节点的小的说明是在左子树,比根节点大的说明在右子树
  • 递归按照数组的开始位置 start 和结束位置 end
  • 递归结束 start>= end 表示只有一个结点,必然没问题,直接返回true
  • 分别按照大小比较去找到左子树的结束位置和右子树的结束位置
  • 判断右子树的结束位置是否正好是根节点,否则就是异常
  • 正常情况下,继续去拆分成左右子树去递归调用

示例代码:

bool dfs(vector<int>& postorder, int start, int end)
{
    // 单个结点,必然是二叉树
    if (start >= end)
    {
        return true;
    }
    // 找左子树的结束位置
    int nIndex = start;
    int rootVal = postorder[end];
    while (postorder[nIndex] < rootVal)
    {
        ++nIndex;
    }
    // 设置左子树结束位置
    int leftEnd = nIndex - 1;
    
    // 找右子树的结束位置
    while (postorder[nIndex] > rootVal)
    {
        ++nIndex;
    }
    
    // 右子树结束位置应该正好在end这里
    return nIndex == end && dfs(postorder, start, leftEnd) && dfs(postorder, leftEnd+1, end-1);
}

bool verifyPostorder(vector<int>& postorder) {
    return dfs(postorder, 0, postorder.size()-1);
}

6、二叉树中和为某一值的路径

题目:

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

示例 1:

 输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]
示例 2:

 输入:root = [1,2,3], targetSum = 5
输出:[]
示例 3:

输入:root = [1,2], targetSum = 0
输出:[]

LeetCode链接:力扣

思路:

  • 按照前序方式去遍历(根,左,右)
  • 递归处理:
  • 忽略空节点
  • 递归过程中去修改sum的结果,一旦发现等于0同时是根节点则返回结果
  • 回溯就是每次先插入当前节点,递归完成后删除

示例代码:

// 维护当前的路径
vector<int> path;
// 结果
vector<vector<int>> res;
vector<vector<int>> pathSum(TreeNode* root, int sum) {
    dfs(root, sum);
    return res;
}

// 递归求解
void dfs(TreeNode* curr, int target)
{
    // 忽略空结点
    if (curr != nullptr)
    {
        target -= curr->val;
        path.push_back(curr->val);
        if (target != 0 || curr->left != nullptr || curr->right != nullptr)
        {
            dfs(curr->left, target);
            dfs(curr->right, target);
        }
        else
        {
            // 满足结果且为根节点
            res.push_back(path);
        }
        // 回溯
        path.pop_back();
    }
}

7、二叉搜索树与双向链表

题目:

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

为了让您更好地理解问题,以下面的二叉搜索树为例:

 我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。

下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。

 特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。

LeetCode链接:力扣

思路:

  • 定义两个指针preheadpre指针用于保存中序遍历的前一个节点,head指针用于记录排序链表的头节点
  • 中序遍历,所以遍历顺序就是双线链表的建立顺序。我们只需要在中序遍历的过程中,修改每个节点的左右指针,将零散的节点连接成双向循环链表
  • 首先遍历二叉树的左子树,然后是当前根节点root

<img src="åæ£500é¢å·é¢ç¬è®°.assets/image-20210715112013923.png" alt="image-20210715112013923" style="zoom:50%;" />

  • 当前驱节点pre不为空时,将前驱节点pre的右指针指向当前根节点root,即pre->right = root
  • 当前驱节点pre为空时: 代表正在访问链表头节点,记为 head = root ,保存头节点
  • 每一个root节点访问时它的左子树肯定被访问过了,因此放心修改它的left指针,将root的left指针指向它的前驱节点,即 root->left = pre, 这样两个节点之间的双向指针就修改好了

<img src="åæ£500é¢å·é¢ç¬è®°.assets/image-20210715112840537.png" alt="image-20210715112840537" style="zoom:50%;" />

  • 然后前驱节点pre右移到当前root节点,接下来递归到右子树重复上述操作

<img src="åæ£500é¢å·é¢ç¬è®°.assets/image-20210715113421440.png" alt="image-20210715113421440" style="zoom:50%;" />

  • 完成以上各步,只是将二叉树变成了双向排序链表,我们还需要将链表的首尾连接到一起,将其变成双向循环排序链表

<img src="åæ£500é¢å·é¢ç¬è®°.assets/image-20210715114040899.png" alt="image-20210715114040899" style="zoom:50%;" />

示例代码:

Node* pre = nullptr, *head = nullptr;
Node* treeToDoublyList(Node* root) {
    if (root == nullptr)
    {
        return root;
    }
    dfs(root);
    head->left = pre;
    pre->right = head;
    return head;
}
void dfs(Node* root){
    if (root == nullptr)
    {
        return root;// 递归边界: 叶子结点返回
    }
    dfs(root->left);  //左子树
    if (pre)
    {
        pre->right = root;
    }
    else
    {
        head = root; // 保存链表头结点
    }
    root->left = pre;
    pre = root;
    dfs(root->right); //右子树
}

8、二叉树深度

 题目:

输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。

例如:

给定二叉树 [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7
返回它的最大深度 3 。

LeetCode链接:力扣

思路:

两种方法

  • (1)递归遍历,判断左右子树高度
  • (2)BFS递归

示例代码:

//递归
  int maxDepth(TreeNode* root) {
        if (root == nullptr)
        {
            return 0;
        }
        int nLeft = maxDepth(root->left);
        int nRight = maxDepth(root->right);
        return nLeft > nRight ? nLeft + 1 : nRight + 1;
    }
//BFS
  int maxDepth(TreeNode* root) {
    if (root == nullptr)
     {
       return 0;
     }
     queue<TreeNode *> que; 
     que.push(root);
     while(que.size())
     {
              
       for(int i = que.size();i >= 0;--i)
        {
           auto tmp = que.front();
           que.pop();
           if(tmp->left) que.push(tmp->left);
           if(tmp->right) que.push(tmp->right);
        }
        count++;
       }
       return count;
    }

9、平衡二叉树

题目:

输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

示例 1:

给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7
返回 true 。

示例 2:

给定二叉树 [1,2,2,3,3,null,null,4,4]

       1
      / \
     2   2
    / \
   3   3
  / \
 4   4
返回 false 。

LeetCode链接:力扣

思路:

  • 对于空结点,深度为0
  • 当前深度是左右子树深度的最大值+1, 有效情况直接返回深度
  • 一旦发现左右子树的深度差异超过1,则认为无效,返回-1
  • 一旦发现返回是-1, 直接返回-1

示例代码:

bool isBalanced(TreeNode* root) {
    return dfs(root) != -1;
}

int dfs(TreeNode* node)
{
    if (node == nullptr)
    {
        return 0;
    }
    int left = dfs(node->left);
    if (left == -1)
    {
        return -1;
    }
    int right = dfs(node->right);
    if (right == -1)
    {
        return -1;
    }
    
    return abs(left-right) > 1 ? -1 : max(left, right) + 1;
}

10、二叉树的下一个结点

题目:

给定一个二叉树其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的next指针

思路:

  • 二叉树为空,则返回空;
  • 节点右孩子存在,则设置一个指针从该节点的右孩子出发,一直沿着指向左子结点的指针找到的叶子节点即为下一个节点;
  • 右孩子不存在,如果节点不是根节点,如果该节点是其父节点的左孩子,则返回父节点;否则继续向上遍历其父节点的父节点,重复之前的判断,返回结果

示例代码:

TreeLinkNode* GetNext(TreeLinkNode* pNode)
{
    if (pNode == nullptr)
        return nullptr;
    TreeLinkNode* node = nullptr;
    if (pNode->right != nullptr) {//如果当前节点有右子树,则右子树最左边的那个节点就是
        node = pNode->right;
        while (node->left != nullptr)
            node = node->left;
        return node;
    }
    node = pNode;
    while (node->next != nullptr && node == node->next->right) {//找到当前节点是其父亲节点的左孩子的那个节点,然后返回其父亲节点,如果当前节点没有右子树
        node = node->next;
    }
    return node->next;
}

11、对称的二叉树

 题目:

请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

    1
   / \
  2   2
 / \ / \
3  4 4  3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

    1
   / \
  2   2
   \   \
   3    3

示例 1:

输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:

输入:root = [1,2,2,null,3,null,3]
输出:false

LeetCode链接:力扣

思路:

  • 通过递归,判断左节点值等于右节点值&&左节点的左孩子等于右节点的右孩子&&左节点的右孩子等于右节点的左孩子

示例代码:

bool isSymmetric(TreeNode* root) {
        if (root == nullptr)
        {
            return true;
        }

        return help(root->left,root->right);
    }
    bool help(TreeNode* leftNode,TreeNode* rightNode)
    {
        if (leftNode == nullptr && rightNode == nullptr)
        {
            return true;
        }
        if (leftNode == nullptr || rightNode == nullptr)
        {
            return false;
        }

        return leftNode->val == rightNode->val && help(leftNode->left,rightNode->right) && help(leftNode->right, rightNode->left);
    }

12、按之字形顺序打印二叉树

题目:

给定一个二叉树,返回该二叉树的之字形层序遍历,(第一层从左向右,下一层从右向左,一直这样交替)

数据范围:0 ≤ n ≤15000,树上每个节点的val满足 |val| <= 100

例如:
给定的二叉树是{1,2,3,#,#,4,5}

该二叉树之字形层序遍历的结果是

[

[1],

[3,2],

[4,5]

]

示例1

输入:{1,2,3,#,#,4,5}

返回值:[[1],[3,2],[4,5]]

示例2

输入:{8,6,10,5,7,9,11}

返回值:[[8],[10,6],[5,7,9,11]]

示例3

输入:{1,2,3,4,5}

返回值:[[1],[3,2],[4,5]]

思路:

两种方式:

  • (1)使用队列
  • 初始情况,根结点入队列;
  • 定义变量size记录当前队列长度;
  • 对于「当前队列」,遍历其所有元素:依次出队列、访问该元素、左右孩子入队列。注意:新入队列的「左右孩子」应当在下一层被访问,因此循环次数为size次。
  • 重复上述步骤直至队列为空
  • (2)使用栈
  • 定义两个栈stk1,stk2;将根结点入栈stk1;
  • 对于下一层,应是「从右至左」打印,因此需要遍历栈stk1、访问每一个元素,并将每一元素的孩子按照「先右孩子、后左孩子」的顺序入栈stk2;将访问的结点保存;
  • 对于下一层,应是「从左至右」打印,因此需要遍历栈stk2、访问每一个元素,并将每一元素的孩子按照「先左孩子、后右孩子」的顺序入栈stk1;将访问的结点保存;
  • 重复上述操作,直至两个栈全为空

 示例代码:

//使用队列
vector<vector<int> > Print(TreeNode* pRoot) {
    vector<vector<int> > ret;
    if (pRoot == nullptr)
    {
        return ret;
    }
    queue<TreeNode*> q; // 定义队列
    q.push(pRoot); // 根结点入队列
    int level = 0;
    while (!q.empty()) {
        vector<int> arr; // 定义数组存储每一行结果
        int size = q.size(); // 当前队列长度
        for (int i = 0; i < size; i++) {
            TreeNode* tmp = q.front(); // 队头元素
            q.pop();
            if (tmp == nullptr) // 空元素跳过
            {
                continue;
            }
            q.push(tmp->left); // 左孩子入队列
            q.push(tmp->right); // 右孩子入队列
            if (level % 2 == 0)
            {
                // 从左至右打印
                arr.push_back(tmp->val);
            } else
            { // 从右至左打印
                arr.insert(arr.begin(), tmp->val);
            }
        }
        level++; // 下一层,改变打印方向
        if (!arr.empty())
        {
            ret.push_back(arr); // 放入最终结果
        }
    }
    return ret;
}
//使用栈
vector<vector<int> > Print(TreeNode* pRoot) {
    vector<vector<int> > ret;
    if (pRoot == nullptr)
    {
        return res;
    }
    stack<TreeNode*> stk1, stk2; // 定义两个栈
    stk1.push(pRoot); // 根结点入栈
    while (!stk1.empty() || !stk2.empty()) { // 两个栈全空时循环结束
        vector <int> arr;
        while (!stk1.empty()) {
            TreeNode* p = stk1.top();
            stk1.pop();
            arr.push_back(p->val); // 访问栈顶
            if (p->left)
            {
                stk2.push(p->left); // 左孩子入栈
            }
            if (p->right)
            {
                stk2.push(p->right); // 右孩子入栈
            }
        }
        if (arr.size())
        {
            res.push_back(arr); // 保存结果
        }
        arr.clear(); // 清空
        while (!stk2.empty()) {
            TreeNode* p = stk2.top();
            stk2.pop();
            arr.push_back(p->val); // 访问栈顶
            if (p->right)
            {
                stk1.push(p->right); // 右孩子入栈
            }
            if (p->left)
            {
                stk1.push(p->left); // 左孩子入栈
            }
        }
        if (arr.size())
        {
            ret.push_back(arr); // 保存结果
        }
    }
    return ret;
}

13、序列化二叉树

题目:

请实现两个函数,分别用来序列化和反序列化二叉树。

你需要设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

提示:输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。

示例:

输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]

LeetCode链接:力扣

思路:

  • 先序遍历即可。遇到空节点就序列化为特殊符号,例如 '#'.
  • 在反序列化的时候,先将字符串解析的过程中将新建的节点存入 vector 容器。
  • 然后根据节点间的顺序关系,构建连接树结构

示例代码:

// Encodes a tree to a single string.
string serialize(TreeNode* root) {
    string str;
    help1(root,str);
    return str;
}
//先序遍历
void help1(TreeNode* root,string &str)
{
    if (root == nullptr)
    {
        str += "#,";
    }
    else
    {
        str +=  to_string(root->val) + ",";
        help1(root->left,str);
        help1(root->right,str);
    }
}

// Decodes your encoded data to tree.
//解析特殊字符,存入vector
TreeNode* deserialize(string data) {
    vector<string> vec;
    string str;
    for (auto &ch : data)
    {
        if (ch == ',')
        {
            vec.push_back(str);
            str = "";
        }
        else
        {
            str.push_back(ch);
        }
    }
    if (!str.empty())
    {
        vec.push_back(str);
        str = "";
    }
    return help2(vec);
}
//递归建树
TreeNode *help2(vector<string> &vec)
{
    if (vec.front() == "#")
    {
        vec.erase(vec.begin());
        return nullptr;
    }
    TreeNode *root = new TreeNode(stoi(vec.front()));
    vec.erase(vec.begin());
    root->left = help2(vec);
    root->right = help2(vec);

    return root;
}

14、把二叉树打印成多行

题目:

给定一个节点数为 n 二叉树,要求从上到下按层打印二叉树的 val 值,同一层结点从左至右输出,每一层输出一行,将输出的结果存放到一个二维数组中返回。

例如:
给定的二叉树是{1,2,3,#,#,4,5}

该二叉树多行打印层序遍历的结果是

[

[1],

[2,3],

[4,5]

]

思路:

两种方式:

  • (1)递归
  • 递归方法通过定义递归函数helper,遍历树的每一层,并将结果放入结果数组res的相应位置,因此需要定义level变量以记录当前访问到的层数。
  • 在完成对某一层的遍历时,按照相同方式递归地访问左孩子和右孩子,且层数加1
  • (2)非递归
  • 初始情况,根结点入队列;
  • 定义变量size记录当前队列长度;
  • 对于「当前队列」,遍历其所有元素:依次出队列、访问该元素、左右孩子入队列。注意:新入队列的「左右孩子」应当在下一层被访问,因此循环次数为size次。

示例代码:

//递归
vector<vector<int> > Print(TreeNode* pRoot) {
    vector<vector<int> > ret;
    if (pRoot == nullptr)
    {
        return ret;
    }
    helper(ret, 0, pRoot);
    return ret;
}
void helper(vector<vector<int> > & ret, int level, TreeNode* node) {
    if (node == nullptr) // 结点为空,直接返回
    {
        return;
    }
    if (ret.size() == level)
    { // 遍历到新的一层时
        vector<int> tmp;
        ret.push_back(tmp);
    }
    ret[level].push_back(node->val); // 保存结果(第level层)
    helper(ret, level + 1, node->left); // 左孩子
    helper(ret, level + 1, node->right); // 右孩子
}
//非递归
vector<vector<int> > Print(TreeNode* pRoot) {
    vector<vector<int> > ret;
    if (pRoot == nullptr)
    {
        return ret;
    }
    queue<TreeNode*> q; // 定义队列
    q.push(pRoot); // 根结点入队
    while (!q.empty()) {
        int size = q.size();
        vector<int> arr;
        for (int i = 0; i < size; i++) {
            TreeNode* p = q.front();
            q.pop();
            if (p->left) q.push(p->left); // 左孩子入队
            if (p->right) q.push(p->right); // 右孩子入队
            if (!p) 
            {
                continue;
            }
            arr.push_back(p->val);
        }
        ret.push_back(arr);
        arr.clear();
    }
    return ret;
}

15、二叉搜索树的第K个节点

题目:

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。

示例 1:

输入:root = [3,1,4,null,2], k = 1
输出:1
示例 2:

输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3

LeetCode链接:力扣

思路:

  • 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  • 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  • 任意节点的左、右子树也分别为二叉查找树
  • 根据二叉搜索树的性质,如果将所有节点的数字从小到大放入到一个数组中num中,答案即为num[k-1]

示例代码:

vector<int> num;
int kthSmallest(TreeNode* root, int k) {
    dfs(root);
    return num[k - 1];
}
void dfs(TreeNode *node) {
    if (node->left != NULL) 
       {
            dfs(node->left);
       } 
    
    num.push_back(node->val);
    
    if (node->right != NULL) 
        {
            dfs(node->right);
        }
    
}

六、动态规划

1、斐波那契数列

 题目:

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:

F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入:n = 2
输出:1
示例 2:

输入:n = 5
输出:5

LeetCode链接:力扣

思路:

两种方法:

  • (1)递归
  • (2)迭代

示例代码:

//递归
int fib(int n) {
    if (n <= 1)
    {
        return n;
    }
    return (fib(n-1) + fib(n-2))%1000000007;
}
//迭代
int fib(int n) {
   if(n <= 0)
    {
       return 0;
    }
    int first = 0;
    int second = 1;
    for (int i = 2; i <=n;i++)
    {
       int temp = first + second;
       first = second;
       second = temp%1000000007;
       std::cout <<first <<second <<std::endl;
     }
    
    return second;
 }

2、跳台阶

 题目:

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入:n = 2
输出:2
示例 2:

输入:n = 7
输出:21
示例 3:

输入:n = 0
输出:1

LeetCode链接:力扣

思路:

  • 动态规划思想,跳1阶,一种方法
  • 跳2阶,两种方法(1阶1阶跳,一次跳2阶)
  • 跳3阶,三种方式(1阶1阶跳,先跳1阶再跳2阶,先跳2阶再跳1阶)
  • ……

示例代码:

int numWays(int n) {
   if (n <=1)
   {
      return 1;
   }
   int first = 1;
   int second = 2;
   for (int i = 3; i <= n;i++)
   {
       int nTemp  = first + second;
       first = second;
       second = nTemp%1000000007;
   }
   return second;
}

3、变态跳台阶

 题目:

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法

思路:

  • 因为n级台阶,第一步有n种跳法:跳1级、跳2级、到跳n级
  • 跳1级,剩下n-1级,则剩下跳法是f(n-1)
  • 跳2级,剩下n-2级
  • f(n)=f(n-1)+f(n-2)+...+f(1) 因为f(n-1)=f(n-2)+f(n-3)+...+f(1) 所以f(n)=2*f(n-1)

示例代码:

//递归
int jumpFloorII(int number) {

    if(number==1) return 1;
    return 2*jumpFloorII(number-1);
}
//迭代
int jumpFloorII(int number) {

   if(number==1) return 1;
   int count=0,a=1;
   for(int i = 2;i<=number;++i){
     count=a*2;
     a=count;
     }
     return count;
 }

4、矩形覆盖

 题目:

我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。

请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

比如n=3时,2*3的矩形块有3种覆盖方法:

 示例代码:

//递归:
int rectCover(int number) {
   if(number<=2) return number;       
   return rectCover(number-1)+rectCover(number-2);
}
//迭代
 int rectCover(int number) {
    if (number <= 2) {
       return number;
    }
    int first = 1, second = 2, third = 3;
    for (int i = 3; i <= number; ++i) {
        third = first + second;
        first = second;
        second = third;
    }
    return third;
}

5、剪绳子

题目:

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m-1] 。请问 k[0]*k[1]*...*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

LeetCode链接:力扣

思路:

  • 采用动态规划方法。定义dp状态dp[i]为绳长为i时,绳子的可能最大长度乘积
  • 对于绳长为i的绳子,我们选择在j位置将绳子剪断,此时绳子被分成长度为j和长度为i - j的两部分,此时剪断之后绳子可能的最大长度乘积,为绳长为j和绳长i - j时候结果的积。而dp状态更新为max(dp[i],dp[j] × dp[i - j])
  • dp数组的长度:由于定义dp[i]为绳长度为i时的结果,因此dp[n]为最后一项,所以数组的总长度为n + 1
  • j的遍历范围:如果j > i/2,那么dp[j]已经由之前的dp[i - j]遍历过,导致重复计算,因此j最多遍历到i/2。同时,由于题目规定必须要切一刀,因此不能使得某一段的长度为0,因此j最少遍历到1。
  • 为什么n = 1,2,3的返回值不等于dp[n]:同第2点所述,当作为结果返回时,必须要切一段,不能不切。但是作为子问题被引用时,可以作为一整段出现,也就是不切,因此最大值有区别

示例代码:

int cuttingRope(int n) 
{
    if(n <= 1)    return 0;
    if(n == 2)    return 1;
    if(n == 3)    return 2;
    
    vector<int>	dp(n + 1,0);
    
    dp[0] = 0;
    dp[1] = 1;
    dp[2] = 2;
    dp[3] = 3;
    
    for(int i = 4;i <= n;i++)
    {
        for(int j = 1;j <= i / 2;j++)
        {
            dp[i] = max(dp[j] * dp[i - j],dp[i]);
        }
    }
    
    return dp[n];
}

七、回溯法

1、矩阵中的路径

题目:

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

例如,在下面的 3×4 的矩阵中包含单词 "ABCCED"(单词中的字母已标出)。

示例 1:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
示例 2:

输入:board = [["a","b"],["c","d"]], word = "abcd"
输出:false

LeetCode链接:力扣

思路:

  • 深度优先搜索: 可以理解为暴力法遍历矩阵中所有字符串可能性。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
  • 剪枝: 在搜索中,遇到 这条路不可能和目标字符串匹配成功 的情况(例如:此矩阵元素和目标字符不同、此元素已被访问),则应立即返回,称之为 可行性剪枝

Picture0.png

DFS 解析:

  • 递归参数: 当前元素在矩阵 board 中的行列索引 i 和 j ,当前目标字符在 word 中的索引 k 。
  • 终止条件:返回 false : (1) 行或列索引越界 或 (2) 当前矩阵元素与目标字符不同 或 (3) 当前矩阵元素已访问过 ( (3) 可合并至 (2) ) 。返回 true : k = len(word) - 1 ,即字符串 word 已全部匹配。
  • 递推工作:标记当前矩阵元素: 将 board[i][j] 修改为 空字符 '' ,代表此元素已访问过,防止之后搜索时重复访问。搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,使用 或 连接 (代表只需找到一条可行路径就直接返回,不再做后续 DFS ),并记录结果至 res 。还原当前矩阵元素: 将 board[i][j] 元素还原至初始值,即 word[k] 。
  • 返回值: 返回布尔量 res ,代表是否搜索到目标字符串。

示例代码:

bool exist(vector<vector<char>>& board, string word) {
    rows = board.size();
    cols = board[0].size();
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            if(dfs(board, word, i, j, 0)) return true;
        }
    }
    return false;
}
int rows, cols;
bool dfs(vector<vector<char>>& board, string word, int i, int j, int k) {
    if(i >= rows || i < 0 || j >= cols || j < 0 || board[i][j] != word[k]) return false;
    if(k == word.size() - 1) return true;
    board[i][j] = '\0';
    bool res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) ||
            dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);
    board[i][j] = word[k];
    return res;
}

2、机器人运动范围

题目:

地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

示例 1:

输入:m = 2, n = 3, k = 1
输出:3
示例 2:

输入:m = 3, n = 1, k = 0
输出:1

LeetCode链接:力扣

思路:

  • 深度优先搜索: 可以理解为暴力法模拟机器人在矩阵中的所有路径。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
  • 剪枝: 在搜索中,遇到数位和超出目标值、此元素已访问,则应立即返回,称之为 可行性剪枝 。
  • 递归参数: 当前元素在矩阵中的行列索引 i 和 j ,两者的数位和 si, sj 。
  • 终止条件: 当 ① 行列索引越界 或 ② 数位和超出目标值 k 或 ③ 当前元素已访问过 时,返回 00 ,代表不计入可达解。
  • 递推工作:
  • 标记当前单元格 :将索引 (i, j) 存入 Set visited 中,代表此单元格已被访问过。
  • 搜索下一单元格: 计算当前元素的 下、右 两个方向元素的数位和,并开启下层递归 。
  • 回溯返回值: 返回 1 + 右方搜索的可达解总数 + 下方搜索的可达解总数,代表从本单元格递归搜索的可达解总数

示例代码;

int movingCount(int m, int n, int k)
{
    vector<vector<bool>> vec(m,vector<bool>(n,false));
    return dfs(0,0,m,n,k,vec);
}
int dfs(int x, int y,int m,int n,int k,vector<vector<bool>> &vec)
{
    if (x < 0 || y< 0 || x >= m || y >= n || vec[x][y] || get(x,y) > k)
    {
        return 0;
    }
    vec[x][y] = true;
    return dfs(x+1,y,m,n,k,vec) + dfs(x-1,y,m,n,k,vec) + dfs(x,y+1,m,n,k,vec) + dfs(x,y-1,m,n,k,vec) + 1;
}
int get(int x, int y)
{
    int ret = 0;
    while(x)
    {
        ret += x %10;
        x = x/10;
    }
    while(y)
    {
        ret += y%10;
        y = y/10;
    }
    return ret;
}

八、数学

1、数值的整数次方

 题目:

给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

保证base和exponent不同时为0。不得使用库函数,同时不需要考虑大数问题,也不用考虑小数点后面0的位数。

示例1

输入

2.00000,3

Copy to clipboardErrorCopied

返回值

8.00000

Copy to clipboardErrorCopied

示例2

输入

2.10000,3

Copy to clipboardErrorCopied

返回值

9.26100

Copy to clipboardErrorCopied

示例3

输入

2.00000,-2

Copy to clipboardErrorCopied

返回值

0.25000

Copy to clipboardErrorCopied

LeetCode链接:力扣

思路:

  • 如果base==0,返回0.0
  • 如果exponent ==0.0,返回1.0
  • 判断exponent是否为负数,如果为负数,转成正数

示例代码:

//直接遍历
 double myPow(double x, int n) {
   if (x == 0) return 0.0;
   if (n == 0) return 1.0;

   bool bRet = false;
   if (n < 0)
   {
      n = n*(-1);
      bRet = true;
   }
   double temp = 1;
   while(n-- >0)
   {
      temp*=x;
   }

   return bRet?1/temp:temp;
}
//快速幂
 double myPow(double x, int n) {
    if( n == 0) return 1;
    if( x == 0.0) return 0;
    long  exp = n;//
    if(n < 0) {
      exp = n* (-1.0);
    } 

    double res = 1.0;
    while(exp != 0){
       if( (exp &1) == 1 ){
          res *=x;
        }
        x *=x;
        exp >>= 1;
    }

    return n<0 ? 1/res: res;
}

2、求1+2+3+……+n

题目:

求 1+2+...+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

示例 1:

输入: n = 3
输出: 6
示例 2:

输入: n = 9
输出: 45

LeetCode链接:力扣

思路:

  • 需利用逻辑与的短路特性实现递归终止。
  • 当n == 0时,(n > 0) && ((sum += Sum_Solution(n - 1)) > 0)只执行前面的判断,为false,然后直接返回0;
  • 当n > 0时,执行sum += Sum_Solution(n - 1),实现递归计算Sum_Solution(n)

示例代码;

int Sum_Solution(int n) {
    int sumNum = n;
    bool ans = (n > 0) && ((sumNum += Sum_Solution(n - 1)) > 0);
    return sumNum;
}

3、不用加减乘除做加法

题目:

写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。

示例:

输入: a = 1, b = 1
输出: 2

LeetCode链接:力扣

思路:

  • 不考虑进位,对每一位相加,0+0,1+1的结果是0,0+1,1+0的结果都是1(这异或的结果一样)

  • 进位:0+0,0+1,1+0都不会产生进位;只有1+1才会向前生产一个进位(此时我们可以想象成两个数先做位与运算,然后再向左移动一位)

  • 把前两步的结果相加:第三步相加的的过程依然是重复前两步,直到不产生进位为止

示例代码:

int add(int a, int b) {
    if (a == 0 || b == 0) {
        return a == 0 ? b : a;
    }

    int sum = 0, carry = 0;

    while (b != 0) { // 当没有进位的时候退出循环
        sum = a ^ b;
        carry = (unsigned int) (a & b) << 1; // C++ 不允许负数进行左移操作,故要加 unsigned int

        a = sum;
        b = carry;
    }

    return a;
}

 4、二进制中1的个数

 题目:

编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为 汉明重量).)。

提示:

请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
在 Java 中,编译器使用 二进制补码 记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数 -3。

示例 1:

输入:n = 11 (控制台输入 00000000000000000000000000001011)
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。
示例 2:

输入:n = 128 (控制台输入 00000000000000000000000010000000)
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
示例 3:

输入:n = 4294967293 (控制台输入 11111111111111111111111111111101,部分语言中 n = -3)
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。

Leecode链接:力扣

思路:

  • 直接遍历
  • 位运算:把一个整数-1,在和原整数与运算,会把该整数最右边一个1变成0,那么整数有多少1,就可以进行多少次操作。
  • 比如:1100,减1后变成1011,1100&1011=1000,重复操作

示例代码:

//遍历
 int hammingWeight(uint32_t n) {
        int nCount = 0;
        while(n > 0)
        {
            if (n % 2 == 1)
            {
                nCount++;
            }
            n = n/2;
        }
        return nCount;
    }
//位运算
 int hammingWeight(uint32_t n) {
        int count=0;
        while(n!=0){
            count++;
            n=n&(n-1);
        }
        return count;
    }

九、其他

1、最小的K个数

 题目:

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例 1:

输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:

输入:arr = [0,1,2,1], k = 1
输出:[0]

LeetCode链接:力扣

思路:

两种方式:

  • (1)从小到大排序,取前k个数
  • (2)使用优先队列,大顶堆

示例代码:

//排序,取k个值
vector<int> getLeastNumbers(vector<int>& arr, int k) {
        if (k <=0 || arr.size() <= 0)
        {
            return vector<int>();
        }
        if (k >= arr.size())
        {
            return arr;
        }

        std::sort(arr.begin(),arr.end())

        vector<int> ret;
        for (int i = 0 ;i < k ;i++)
        {
            ret.push_back(arr[i]);
        }

        return ret;
    }

//优先队列
vector<int> getLeastNumbers(vector<int>& arr, int k) {
        if (k <=0 || arr.size() <= 0)
        {
            return vector<int>();
        }
        if (k >= arr.size())
        {
            return arr;
        }

        priority_queue<int ,vector<int>, greater<int>> pq;
        for (auto & a : arr)
        {
            pq.push(a);
        }

        vector<int> ret;
        while(k--)
        {
            ret.push_back(pq.top());
            pq.pop();
        }

        return ret;
    }

2、从1到n整数中1出现的次数

题目:

输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。

例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

示例 1:

输入:n = 12
输出:5
示例 2:

输入:n = 13
输出:6

LeetCode链接:力扣

思路:

  • 从个位开始计算起,所以我们首先让 digit = 1(表示个位);high = n / 10(表示个位左边的全部);low = 0(表示个位右边的全部,也就是没有);cur = n % 10(表示个位上的数)。
  • 然后循环来移动 cur,确保 n 的每一位都被计算过。每次循环都用 cur 当前的值来匹配上述的三个公式,从而计算该位 1 一共能出现的次数。
  • 注意我们循环退出的条件是 high 和 cur 都等于 0。因为都等于 0 就意味着 n 已经彻底遍历完了。不然的话:
  • 假如退出的条件只有 high 等于 0:那假如 n = 1024,然后此时 cur 在千位,则 cur = 1,high = 0,low = 024。如果此时退出循环,我们还没来得及计算千位能出现的 1 的次数。
  • 假如退出的条件只有 cur 等于 0:那假如 n = 1024,然后此时 cur 在百位,则 cur = 0,high = 1,low = 24。如果此时退出循环,我们还没来得及计算百位和千位能出现的 1 的次数。

示例代码:

int countDigitOne(int n) {
    long digit = 1; // digit 需为 long 型,因为比如 n 是 INT_MAX,digit 会在最后一次循环越界
    int high = n / 10, cur = n % 10, low = 0;
    int res = 0;

    while (high != 0 || cur != 0) {
        if (cur == 0) {
            res += high * digit;
        }
        else if (cur == 1) {
            res += high * digit + low + 1;
        }
        else {
            res += (high + 1) * digit;
        }

        low += cur * digit;
        cur = high % 10;
        high /= 10;
        digit *= 10;
    }

    return res;
}

3、丑数

题目:

我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。

示例:

输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。

LeetCode链接:力扣

思路:

  • 任何一个丑数,都是可以由其前面的某个丑数乘2,或乘3,或乘5得到。
  • 先建立一个长度为 n 的 dp 数组,dp[i] 表示第 i + 1 个丑数。
  • 然后设立三个变量:two, three, five,它们都代表 dp 数组中的索引。通过选取 dp[two] * 2, dp[three] * 3 和 dp[five] * 5 中的最小值来确定下一个丑数的值。
  • 当找到了下一个丑数后,假如这个丑数是由 two 得来的,就 ++ two;假如是由 three 得来的,就 ++ three;是由 five 得来的,我们就 ++ five。这样就可以保证每个丑数都会被找出来。

示例代码:

int nthUglyNumber(int n) {
    int two = 0, three = 0, five = 0;
    vector<int> dp(n);
    dp[0] = 1; // dp 初始化

    for (int i = 1; i < n; ++i) {
        int t1 = dp[two] * 2, t2 = dp[three] * 3, t3 = dp[five] * 5;
        dp[i] = min(min(t1, t2), t3);

        if (dp[i] == t1) {++ two;}
        if (dp[i] == t2) {++ three;}
        if (dp[i] == t3) {++ five;}
    }

    return dp[n - 1];
}

4、和为S的连续正数序列

题目:

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

示例 1:

输入:target = 9
输出:[[2,3,4],[4,5]]
示例 2:

输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]

LeeCode链接:力扣

思路:

  • 出始化: 左边界 i=1 ,右边界 j = 2,元素和 s = 3 ,结果列表 res ;
  • 循环: 当 i ≥j 时跳出;
  • 当 s > targets 时: 向右移动左边界 i = i + 1 ,并更新元素和 s;
  • 当 s < targets 时: 向右移动右边界 j = j + 1 ,并更新元素和 s;
  • 当 s = targets 时: 记录连续整数序列,并向右移动左边界 i = i + 1;
  • 返回值: 返回结果列表 res

示例代码:

vector<vector<int>> findContinuousSequence(int target) {
    int i = 1, j = 2, s = 3;
    vector<vector<int>> res;
    while(i < j) {
        if(s == target) {
            vector<int> ans;
            for(int k = i; k <= j; k++)
                ans.push_back(k);
            res.push_back(ans);
        }
        if(s >= target) {
            s -= i;
            i++;
        } else {
            j++;
            s += j;
        }
    }
    return res;
}

5、和为S的两个数字

 题目:

输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
示例 2:

输入:nums = [10,26,30,31,47,60], target = 40
输出:[10,30] 或者 [30,10]

LeetCode链接:力扣

思路:

  • 通过双指针,定义left和right,分别指向数组头和尾
  • 判断nums[left]+nums[right]和s比较
  • 等于s,返回nums[left]和num[right]
  • 小于s,left++后移
  • 大于s,right--前移

示例代码:

   vector<int> twoSum(vector<int>& nums, int target) {
        vector<int> vecTemp(2,-1);
        int nLeft = 0;
        int nRight = nums.size()-1;
        while(nLeft <= nRight)
        {
            if (nums[nLeft] + nums[nRight] == target)
            {
                vecTemp[0] = nums[nLeft];
                vecTemp[1] = nums[nRight];
                break;
            }
            else if (nums[nLeft] + nums[nRight] > target)
            {
                nRight--;
            }
            else if (nums[nLeft] + nums[nRight] < target)
            {
                nLeft++;
            }
        }

        return vecTemp;
    }

6、扑克牌顺子

题目:

从若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。

示例 1:

输入: [1,2,3,4,5]
输出: True

示例 2:

输入: [0,0,1,2,5]
输出: True

LeetCode链接:力扣

思路:

  • 先排序
  • 统计0的个数,和非零元素差值
  • 判断差值小于等于0的个数

示例代码:

bool isStraight(vector<int>& nums) {
    sort(nums.begin(),nums.end());
    int nCount = 0;
    int nSum = 0;
    for (int nIndex = 0;nIndex < nums.size()-1;nIndex++)
    {
        if (nums[nIndex] == 0)
        {
            nCount++;
            continue;
        }
        if (nums[nIndex] == nums[nIndex+1])
        {
            return false;
        }   
        nSum += nums[nIndex+1] - nums[nIndex] -1;
    }
    
    return nSum <= nCount;
}

7、圆圈中最后剩下的数

题目:

0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

示例 1:

输入: n = 5, m = 3
输出: 3
示例 2:

输入: n = 10, m = 17
输出: 2

LeetCode链接:力扣

思路:

  • 整个过程中,每去掉一个数字,相当于把剩下的数字的下标向前移动了m位。
  • 从而得出f(n, m) = (f(n - 1, m) + m) % n
  • 推导公式:

示例代码:

//约瑟夫问题
int lastRemaining(int n, int m) {
    int pos = 0; // 最终活下来那个人的初始位置
    for(int i = 2; i <= n; i++){
        pos = (pos + m) % i;  // 每次循环右移
    }
    return pos;
}

8、数据流中的中位数

题目:

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。
示例 1:

输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]
示例 2:

输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]

LeetCode链接:力扣

思路:

  • 每插入一个数之前,先判断两个堆的 size() 是否相等。

  • 若相等,先将这个数插入大顶堆,然后将大顶堆的 top() 插入小顶堆。这么做可以保证小顶堆的所有数永远大于等于大顶堆的 top()。

  • 若不相等,先将这个数插入小顶堆,然后将小顶堆的 top() 插入大顶堆。这么做可以保证大顶堆的所有数永远小于等于小顶堆的 top()。

  • 整个过程我们都动态地做到了平衡两个堆的 size(),即保证它们的 size() 最大只相差了 1

示例代码:

priority_queue<int, vector<int>, less<int> > maxheap;
priority_queue<int, vector<int>, greater<int> > minheap;

void addNum(int num) {
    if(maxheap.size() == minheap.size()) {
        maxheap.push(num);
        minheap.push(maxheap.top());
        maxheap.pop();
    }
    else {
        minheap.push(num);
        maxheap.push(minheap.top());
        minheap.pop();
    }
}

double findMedian() {
    int maxSize = maxheap.size(), minSize = minheap.size();
    int mid1 = maxheap.top(), mid2 = minheap.top();
    return maxSize == minSize ? ((mid1 + mid2) * 0.5) : mid2;
}

9、滑动窗口的最大值

题目:

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7] 
解释: 

  滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

LeetCode链接:力扣

思路:

两种实现方法:

(1)优先队列

  • 将数组nums的前 k个元素放入优先队列中。每当我们向右移动窗口时,我们就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值
  • 然而这个最大值可能并不在滑动窗口中,在这种情况下,这个值在数组 nums中的位置出现在滑动窗口左边界的左侧。因此,当我们后续继续向右移动窗口时,这个值就永远不可能出现在滑动窗口中了,我们可以将其永久地从优先队列中移除
  • 不断地移除堆顶的元素,直到其确实出现在滑动窗口中。此时,堆顶元素就是滑动窗口中的最大值。为了方便判断堆顶元素与滑动窗口的位置关系,我们可以在优先队列中存储二元组 {num,index},表示元素num在数组中的下标为 index

(2)单调队列

  • 当滑动窗口向右移动时,只要 i还在窗口中,那么 j一定也还在窗口中,这是 i 在 j 的左侧所保证的。因此,由于nums[j]的存在,nums[i] 一定不会是滑动窗口中的最大值了,我们可以将nums[i] 永久地移除
  • 可以使用一个队列存储所有还没有被移除的下标。在队列中,这些下标按照从小到大的顺序被存储,并且它们在数组nums 中对应的值是严格单调递减的。因为如果队列中有两个相邻的下标,它们对应的值相等或者递增,那么令前者为 i,后者为 j,就对应了上面所说的情况,即 nums[i]会被移除,这就产生了矛盾
  • 当滑动窗口向右移动时,我们需要把一个新的元素放入队列中。为了保持队列的性质,我们会不断地将新的元素与队尾的元素相比较,如果前者大于等于后者,那么队尾的元素就可以被永久地移除,我们将其弹出队列。我们需要不断地进行此项操作,直到队列为空或者新的元素小于队尾的元素。
  • 由于队列中下标对应的元素是严格单调递减的,因此此时队首下标对应的元素就是滑动窗口中的最大值。但与方法一中相同的是,此时的最大值可能在滑动窗口左边界的左侧,并且随着窗口向右移动,它永远不可能出现在滑动窗口中了。因此我们还需要不断从队首弹出元素,直到队首元素在窗口中为止。
  • 为了可以同时弹出队首和队尾的元素,我们需要使用双端队列。满足这种单调性的双端队列一般称作「单调队列」

示例代码:

//优先队列
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
    int n = nums.size();
    priority_queue<pair<int, int>> q;
    for (int i = 0; i < k; ++i) {
        q.emplace(nums[i], i);
    }
    vector<int> ans = {q.top().first};
    for (int i = k; i < n; ++i) {
        q.emplace(nums[i], i);
        while (q.top().second <= i - k) {
            q.pop();
        }
        ans.push_back(q.top().first);
    }
    return ans;
}
//单调队列
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
    if (nums.size() <= 0 || k <= 0 || nums.size() < k)
    {
        return {};
    }
    vector<int> ret;
    deque<int> q;
    for (int i = 0 ;i < nums.size(); i++)
    {
        while(!q.empty() && nums[i] > nums[q.back()])
        {
            q.pop_back();
        }
        while(!q.empty() && i - q.front() >= k)
        {
            q.pop_front();
        }
        q.push_back(i);
        if (i  + 1 >= k)
        {
            ret.push_back(nums[q.front()]);
        }
    }
    return ret;
}
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值