LeetCode探索 之 数据结构卡片-数组与字符串 题目

LeetCode探索 之 数据结构卡片-数组和字符串 题目

编程语言:C++

第一部分-数组简介

1. 寻找数组的中心索引

自己做法总结:

  1. 考虑元素个数小于2的情况
  2. 索引下标考虑左边无元素和右边无元素的情况
  3. 利用变量累加值,而不是每一次都暴力对两侧sum进行求取
  4. 利用vector.size()函数计算容器元素个数

下面第一个程序存在bug,不过系统可以通过

class Solution {
public:
    int pivotIndex(vector<int>& nums) {
        //获取数组个数
        int N=nums.size();
        /**********下面这个说法是错误的,故需要更正**********/
        //元素少于三个是没有中心索引的
        if(N<=2)
            return -1;
        else
        {
            //前累加和、后累加和
            int sumpre=0;
            int sumpos=0;
            for(int i=1;i<N;i++)
            {
                sumpos+=nums[i];
            }

            //从下标0扫描到下标N-1
            int j;
            for(j=0;j<N;j++)
            {
                if(sumpre==sumpos)
                    break;
                else
                {
                    //向后累加
                    sumpre+=nums[j];
                    //向后累减
                    sumpos-=nums[j+1];
                }
            }
        
            if(j==N)
                return -1;
            else
                return j;
        }
    }
};

更正程序

class Solution {
public:
    int pivotIndex(vector<int>& nums) {
        //获取数组个数
        int N=nums.size();

        //前累加和、后累加和
        int sumpre=0;
        int sumpos=0;
        for(int i=1;i<N;i++)
        {
            sumpos+=nums[i];
        }
        //从下标0扫描到下标N-1
        int j;
        for(j=0;j<N;j++)
        {
            if(sumpre==sumpos)
                break;
            else
            {
                //向后累加
                sumpre+=nums[j];
                //向后累减
                sumpos-=nums[j+1];
            }
        }
        
        if(j==N)
            return -1;
        else
            return j;
    }
};

大佬做法总结:

  1. 充分利用了sum的不变性与中心索引两侧的对称性加速求解简化运算,能找到 sumleft*2 + nums[i] == sum 关系简直太秀了
  2. size(nums)可以得到数组长度
class Solution {
public:
    int pivotIndex(vector<int>& nums) {
        int sum=0;
        int sumleft=0;
        int len=size(nums);
        //计算sum
        for(int i=0;i<len;i++)
        {
            sum+=nums[i];
        }
        //从前往后扫描
        for(int j=0;j<len;j++)
        {
            if(sumleft*2+nums[j]==sum)
                return j;
            else
                sumleft+=nums[j];
        }
        return -1;
    }
};

2 . 搜索插入位置

个人思路总结:

  1. i下标的插入条件:target<=nums[i],否则就应该一直往后再比较
  2. 如果最后都没有找到插入位置,直接返回长度就行,无需进行if判断。同时此处也体会到return语句必须在书写的时候就应该保证都能执行到,而不是逻辑认为的执行到。
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        //获取数组个数
        int len=size(nums);
        //扫描
        int i;
        for(i=0;i<len;i++)
        {
            if(target<=nums[i])
                return i;
        }

        return len;
    }
};

大佬解法:二分查找

这里可以看一下这个 https://www.zhihu.com/question/36132386

  1. 直接记这个模板就可以
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        //求非降序范围[first, last)内第一个不小于value的值的位置
        int len=size(nums);
        int first=0;
        int last=len;
        int mid;
        while(first<last)//搜索区间[first, last)不为空
        {
            mid=first+(last-first)/2;//防溢出
            if(nums[mid]<target)
                first=mid+1;
            else
                last=mid;
        }
        return first;//last也行,因为[first, last)为空的时候它们重合
    }
};

3合并区间

个人思路总结

  1. 基于上锁-开锁的思想
  2. 建立二维数组temp
  3. 对原二维数组依据【L,R】中的L升序排列
  4. 首先往temp中增添一条【L,R】的信息(最小的L那条),然后进行上锁。上锁之后,如果没有解锁,是无法重新往temp中添加新信息【L,R】的。
  5. 下面是更新temp增添的新信息【L,R】的过程:往后遍历原数组intervals,此过程中记录出现的最大R,一直到目前为止最大的R小于下一条信息L的时候,中止。更新temp中信息的R,并进行解锁。
  6. 开始新一轮的temp插入【L,R】(intervals遍历位置下一条信息),并更新R的过程,循环往复。直至剩下最后一条信息。
  7. 如果最后一条信息为止还没解锁,则需要利用最后一条信息更新temp的R,如果已经解锁,则直接往temp中添加一条信息。

Tips

  1. 二维数组建立 vector<vector> temp
  2. 二维数组行数获取 intervals.size();
  3. 二维数组【L,R】中L升序排列 sort(intervals.begin(), intervals.end());
  4. 二维数组增添一行 temp.push_back({intervals[0][0],intervals[0][1]});
  5. 二维数组更新行信息 temp.back()[1]=maxR;
class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        //二维数组
        vector<vector<int>> temp;
        //获取原数组行数
        int row=intervals.size();
        sort(intervals.begin(), intervals.end());
        //扫描
        int j=0;
        int flag=0;
        int maxR=0;
        if(row==0)
        {
            return {};
        }
        else if(row==1)
        {
            temp.push_back({intervals[0][0],intervals[0][1]});
        }
        else
        {
            int i;
            for(i=0;i<row-1;i++)
            {
                //保留第一元素,上锁
                if(flag==0)
                {
                    temp.push_back({intervals[i][0],intervals[i][1]});
                    flag=1;
                }
                //记录目前所有[L,R]最大的R
                maxR=max(maxR,intervals[i][1]);
                //如果存在数组存在间距,解锁并保留第二元素
                if(flag==1)
                {
                    if(maxR<intervals[i+1][0])
                    { 
                        temp.back()[1]=maxR;
                        flag=0;
                    }
                }
            }
            maxR=max(maxR,intervals[i][1]);
            if(flag==1)//如果没有解锁,则需要针对最后一个 
            {
                temp.back()[1]=maxR;
            }
            else
                temp.push_back({intervals[i][0],intervals[i][1]});
        }

        return temp;
    }
};

大佬做法

做法一总结:

  1. 与个人思路基本一致,都是往新数组temp中写入信息,然后遍历原数组intervals进行更新temp顶部的R。优点在于他是以遍历的intervals【L,R】去跟新数组temp的顶部元素去比较,以比较决定往temp中增加信息,而我是以比较来更新temp顶部的R,感觉他的思路更清晰也更简洁。
  2. 第一次和temp顶部R < intervals的L的时候往temp中增添信息,否则就一致更新temp顶部的R
class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        //空数组直接返回
        if(intervals.size()==0)
        {
            return {};
        }
        vector<vector<int>> temp;
        //依据L排序
        sort(intervals.begin(),intervals.end());
        for(int i=0;i<intervals.size();i++)
        {
            int L=intervals[i][0],R=intervals[i][1];
            if(!temp.size()||temp.back()[1]<L)//只有在第一次或者temp顶部信息R<L的时候写入temp
            {
                temp.push_back({L,R});
            }
            else//更新
            {
                temp.back()[1]=max(temp.back()[1],R);
            }
        }
        return temp;
    }
};

解法二总结

基本思路:排序+双指针

  1. 左指针指向原数组L,右指针指向原数组R,向后遍历,更新maxR,直至遇到间断。
  2. 将信息【L,maxR】插入新数组
class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<vector<int>> temp;
        sort(intervals.begin(),intervals.end());
        for(int i=0;i<intervals.size();)
        {
            int maxR=intervals[i][1];
            int j=i+1;
            while(j<intervals.size()&&maxR>=intervals[j][0])
            {
                maxR=max(maxR,intervals[j][1]);
                j++;
            }
            temp.push_back({intervals[i][0],maxR});
            i=j;
        }
        return temp;
    }
};

第二部分-二维数组简介

1旋转矩阵

方法一 :使用辅助数组
个人思路总结

  1. 旋转矩阵就是第i行放到第size-1-i列
  2. 需要借助一个新二维数组进行旋转后元素的存放,最后再返还给原二维数组
  3. 时间复杂度O(N^2)
  4. 空间复杂度O(N^2)
class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        //获取矩阵行数
        int size=matrix.size();
        //矩阵的拷贝
        auto matrix_new=matrix;
        //行扫描
        for(int i=0;i<size;i++)
        {
            for(int j=0;j<size;j++)
            {
                matrix_new[j][size-1-i]=matrix[i][j];//i行变成size-1-i列
            }
        }
        //矩阵的反拷贝
       matrix=matrix_new;
    }
};

官方其他的做法

方法二:原地旋转

个人思路总结:
利用旋转的对称性,一个大矩阵其实是由4个小矩阵组成的。可以通过遍历左上角的矩阵,利用旋转关系实现扫描整个大矩阵。

  • 时间复杂度O(N^2),N/2 * (N+1)/2 ~ N^2
  • 空间复杂度O(1),只额外多了一个变量temp

下面的旋转关系看似复杂,其实就是通过整个顺序进行赋值的

  • temp=左上角
  • 左上角=左下角
  • 左下角=右下角
  • 右下角=右上角
  • 右上角=temp
    下标关系还是利用了第i行放到第size-1-i
    后面的利用整体思想, [表达式1] [表达式2] -> [表达式2][size-1-表达式1]
    在这里插入图片描述
class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        //获取矩阵的行数
        int size=matrix.size();
        //需要旋转的单位长度
        int i_size,j_size;
        //奇数
        if(size%2)
        {
            i_size=size/2;
            j_size=size/2+1;
        }
        else
        {
            i_size=size/2;
            j_size=size/2;
        }
        //中间变量
        int temp;
        //左上角区域
        for(int i=0;i<i_size;i++)
        {
            for(int j=0;j<j_size;j++)
            {
                temp=matrix[i][j];
                matrix[i][j]=matrix[size-1-j][i];
                matrix[size-1-j][i]=matrix[size-1-i][size-1-j];
                matrix[size-1-i][size-1-j]=matrix[j][size-1-i];
                matrix[j][size-1-i]=temp;
            }
        }
    }
};

看了官方的写法的体会:
4 * 4矩阵的小旋转矩阵是22的矩阵
5 * 5的矩阵的小旋转矩阵是2
3的矩阵
所以行都是size/2,列则是(size+1)/2

class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        for (int i = 0; i < n / 2; ++i) {
            for (int j = 0; j < (n + 1) / 2; ++j) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[n - j - 1][i];
                matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
                matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
                matrix[j][n - i - 1] = temp;
            }
        }
    }
};

方法三:翻转矩阵

旋转矩阵实际上是进行一次行对称翻转+一次对角线翻转

  • 时间复杂度O(N^2),两次 N^2 的扫描
  • 空间复杂度O(1),原地旋转
class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        //获取矩阵的行数
        int size=matrix.size();
        
        //上下翻转
        for(int i=0;i<size/2;i++)
        {
            for(int j=0;j<size;j++)
            {
                swap(matrix[i][j],matrix[size-1-i][j]);
            }
        }
        //对角线翻转
        for(int i=0;i<size;i++)
        {
            for(int j=0;j<i;j++)
            {
                swap(matrix[i][j],matrix[j][i]);
            }
        }
    }
};

2零矩阵

个人naive做法总结

  • 扫描+过渡矩阵
  • 时间复杂度O(N^2), N^2 的扫描
  • 空间复杂度O(N^2)
class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        //过渡矩阵
        vector<vector<int>> matrix_new;
        matrix_new=matrix;
        //获取矩阵的行数和列数
        int row=matrix.size();
        int col=matrix[0].size();
        int flag; //0标志
        //scan
        for(int i=0;i<row;i++)
        {
            for(int j=0;j<col;j++)
            {
                if(!matrix[i][j])
                {
                    flag=1;
                }

                if(flag)
                {
                    //行赋值0
                    for(int k=0;k<col;k++)
                    {
                        matrix_new[i][k]=0;
                    }
                    //列赋值0
                    for(int k=0;k<row;k++)
                    {
                        matrix_new[k][j]=0;
                    }
                    flag=0;
                }
            }
        }
        matrix=matrix_new;
    }
};

改进思路:前面赋值0的行和列无需再进行扫描

  • 时间复杂度O(N^2), N^2 的扫描
  • 空间复杂度O(N),2个N元素一维数组
class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        //获取行数和列数
        int mrow=matrix.size();
        int mcol=matrix[0].size();
        if(mrow==0||mcol==0)
            return;
        //两个一维数组
        vector<bool> row(mrow,false);
        vector<bool> col(mrow,false);
        //扫描
        for(int i=0;i<mrow;i++)
        {
            for(int j=0;j<mcol;j++)
            {
                if(!matrix[i][j])
                {
                    row[i]=true;
                    col[j]=true;
                }
            }
        }
        //矩阵输出        
        for(int i=0;i<mrow;i++)
        {
            for(int j=0;j<mcol;j++)
            {
                if(row[i]||col[j])
                    matrix[i][j]=0;
            }
        }

    }
};

3对角线遍历

个人思路总结:

  • 基本思路:按照对角线扫描的顺序去复现,将元素存放至一维数组
  • 需要有一个flag变量区分奇数和偶数,分清是向上扫描还是向下扫描
  • 非边界:向上扫描整体应该是i自减,j自加
  • 非边界:向下扫描整体应该是i自加,j自减
  • 边界处理:向上扫描至右上角,下一步应该i自加,j不变;向上扫描至上边界,i不变,j自加;向上扫描至右边界,下一步应该j不变,i自增
  • 边界处理:向下扫描至左下角,下一步应该j自加,i不变;向下扫描至下边界,i不变,j自加;向下扫描至左边界,下一步应该j不变,i自增

一些做题的雷区

  • 获取行数之后需要直接做0判断处理,不可以不进行处理去获取列数。因为没有行的时候,是不存在matrix[0]的
class Solution {
public:
    vector<int> findDiagonalOrder(vector<vector<int>>& matrix) {
        //建立一维数组
        vector<int> nums;
        //获取行数和列数//一定要分开书写,因为没行的时候是不会有matrix[0]
        int row=matrix.size();
        if(row==0) 
            return nums;
        int col=matrix[0].size();
        if(col==0) 
            return nums;
        
        int flag=1;//奇数偶数标志位
        for(int i=0,j=0; ;)
        {
            nums.push_back(matrix[i][j]);
            if((i==row-1)&&(j==col-1))
                break;
            //转折点处i与j的更新
            if(flag%2)//奇数上走
            {
                if((i==0)&&(j==col-1))//遇到右上角点
                {
                    i++;
                    flag++;
                }
                else if(i==0)//遇到上边界点
                {
                    j++;
                    flag++;
                }
                else if(j==col-1)//遇到右边界点
                {
                    i++;
                    flag++;
                }
                else
                {
                    i--;
                    j++;
                }
            }
            else//偶数下走
            {
                if((j==0)&&(i==row-1))//遇到左下角点
                {
                    j++;
                    flag++;
                }
                else if(i==row-1)//遇到下边界点
                {
                    j++;
                    flag++;
                }
                else if(j==0)//遇到左边界点
                {
                    i++;
                    flag++;
                }
                else
                {
                    i++;
                    j--;
                }
            }
        }
        return nums;
    }
};

看了官方解析后的一些改进:

主要是针对边界处理,以上行为例,之前是分为上边界、右边界、右上角三种情况进行处理。

缩减为上边界和右边界两种情况,但是if判断时必须右边界先进行判断,再判断上边界
在这里插入图片描述
在这里插入图片描述

class Solution {
public:
    vector<int> findDiagonalOrder(vector<vector<int>>& matrix) {
        //建立一维数组
        vector<int> nums;
        //获取行数和列数//一定要分开书写,因为没行的时候是不会有matrix[0]
        int row=matrix.size();
        if(row==0) 
            return nums;
        int col=matrix[0].size();
        if(col==0) 
            return nums;
        
        int flag=1;//奇数偶数标志位
        for(int i=0,j=0; ;)
        {
            nums.push_back(matrix[i][j]);
            if((i==row-1)&&(j==col-1))
                break;
            //转折点处i与j的更新
            if(flag%2)//奇数上走
            {
                if(j==col-1)//遇到右边界点
                {
                    i++;
                    flag++;
                }
                else if(i==0)//遇到上边界点
                {
                    j++;
                    flag++;
                }
                else
                {
                    i--;
                    j++;
                }
            }
            else//偶数下走
            {
                if(i==row-1)//遇到下边界点
                {
                    j++;
                    flag++;
                }
                else if(j==0)//遇到左边界点
                {
                    i++;
                    flag++;
                }
                else
                {
                    i++;
                    j--;
                }
            }
        }
        return nums;
    }
};

大佬解法

  • 具体详见C++题解
  • 基本思想:寻找到了x+y=对角线被扫描的次数。其实还是分奇数和偶数两种大情况,但是最终利用一套代码进行了实现。
class Solution {
public:
    vector<int> findDiagonalOrder(vector<vector<int>>& matrix) {
        //一维数组
        vector<int> result;
        //获取行数和列数
        int row=matrix.size();
        if(row==0) return result;

        int col=matrix[0].size();
        if(col==0) return result;

        int i=0;//第几条对角线
        bool flag=true;
        while(i<row+col-1)//对角线扫描次数
        {
            int pm=flag ? row:col;
            int pn=flag ? col:row;

            int x=i<pm ? i:pm-1;//选取最大的x
            int y=i-x;//x+y=对角线次数

            while(x>=0 && y<pn)
            {
                result.push_back(flag ? matrix[x][y]:matrix[y][x]);
                x--;
                y++;
            }

            i++;
            flag=!flag;
        }

        return result;
    }
};

第三部分-字符串简介

1.最长公共前缀

个人做法总结

  • 以第一个字符串为基准,后面的依次与其进行比较,每次记录目前为止最大前缀的下标k,之后的扫描只比较前k个字符。
  • 时间复杂度O(MN), M个字符串,平均长度N,每个字符串的每个字符都会被比较
  • 空间复杂度O(1),使用的额外空间为常数
class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        //最大前缀字符串
        string strpre;
        //如果少于1个字符串,输出空。
        if(strs.size()<1)
            return "";
        else if(strs.size()==1)//如果等于1个字符串,输出这个字符串。
            return strs[0];

        int len=strs[0].size();
        //寻找最大前缀子串
        for(int i=1,k=0;i<strs.size();i++)
        {
            for(k=0;k<len;k++)
            {
                if(strs[i][k]==strs[0][k])
                {    
                    strpre+=strs[0][k];
                }
                else
                    break;
            }
            len=k;
            /新添入,最大子串空的时候提前结束//
            if(len<1)
                break;
			/
            if(i!=strs.size()-1)
                strpre="";
        }
        return strpre;
    }
};

官方做法一复现:横向比较

总结:

  • 主要技巧在于写了一个函数比较两个字符串(当前最大前缀子串 与 第i个字符串)。
  • 字符串的比较while这种写法估计会很重要可以记住。
  • prefix=str1.substr(0,index)这种截取字符串的写法
  • 中间过程中会依据当前最大子串是否为空而提前退出
  • 时间复杂度O(MN), M个字符串,平均长度N,每个字符串的每个字符都会被比较
  • 空间复杂度O(1),使用的额外空间为常数在这里插入图片描述
class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        //没有字符串时
        if(strs.size()<1)
            return "";
        //大于等于1个字符串的情况
        string prefix=strs[0];
        for(int i=1;i<strs.size();i++)
        {
            prefix=longestCommonPrefix(prefix, strs[i]);
            if(!prefix.size())
                break;
        }

        return prefix;     
    }
    string longestCommonPrefix(const string& str1, const string& str2)
    {
        //取两个字符串较短的长度
        int len=min(str1.size(),str2.size());
        //while比较
        int index=0;
        while(index<len && str1[index]==str2[index])
        {
            index++;
        }
        //妙处
        return str1.substr(0,index);
    }
};

官方做法二-纵向比较

  • 以第一个字符串为基准,每次取一个字符c
  • 扫描后面的字符串对应位置的字符每次与上面的字符c进行比较
  • 时间复杂度O(MN), M个字符串,平均长度N,每个字符串的每个字符都会被比较
  • 空间复杂度O(1),使用的额外空间为常数
    在这里插入图片描述
class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        //没有字符串时
        if(strs.size()<1)
            return "";
        //获取第一个字符串的长度
        int length=strs[0].size();
        //获取字符串个数
        int count=strs.size();
        //纵向比较
        for(int i=0;i<length;i++)
        {
            int c=strs[0][i];
            for(int j=1;j<count;j++)
            {
                //当字符串长度跟i相等或者字符串的字符不等于c字符的时候就直接返回字符串前缀
                if(i==strs[j].size() || strs[j][i]!=c)
                {
                    return strs[0].substr(0,i);
                }
            }
        }

        return strs[0];     
    }

};

官方做法三-分而治之

总结:

  1. 主要在于递归的终止条件要记得先写
    在这里插入图片描述
  2. 时间复杂度:O(mn)。其中 m 是字符串数组中的字符串的平均长度,n 是字符串的数量。时间复杂度的递推式是T(n)=T(n/2)+O(m),通过计算可得 T(n)=O(mn)
  3. 空间复杂度:O(m logn),其中 m 是字符串数组中的字符串的平均长度,n 是字符串的数量。空间复杂度主要取决于递归调用的层数,层数最大为logn,每层需要 m的空间存储返回结果
class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        if(!strs.size())
            return "";
        else
            return longestCommonPrefix(strs, 0, strs.size()-1);
    }

    string longestCommonPrefix(vector<string>& strs, int start, int end) {
        if(start==end)
            return strs[start];
        else
        {
            int mid=(start+end)/2;
            string leftLCP=longestCommonPrefix(strs,start,mid);   
            string RightLCP=longestCommonPrefix(strs,mid+1,end);   
            return CommonPrefix(leftLCP,RightLCP);
        }
    }

    string CommonPrefix(const string& leftLCP, const string& RightLCP){
        int minlength=min(leftLCP.size(),RightLCP.size());
        for(int i=0;i<minlength;i++)
        {
            if(leftLCP[i]!=RightLCP[i])
                return leftLCP.substr(0,i);
        }
        return leftLCP.substr(0,minlength);
    }
};

2.最长回文子串

自己没有做出
官方做法一动态规划
总结:

  • 最大的教训:s.substr(begin,maxlen)这个函数的用法真的恶心到了,与java的写法不太一样。表示从begin下标开始的maxlen个字符。
  • 一个字符肯定是回文串,所以先赋值对角线1。之后按列填表,先看两个端点i和j,如果不相等肯定不是回文串,如果相等则需要分两种情况。第一种情况去掉两个端点i和j之后就剩一个字符,则肯定是回文串;第二种情况,去掉两个端点i和j之后,依据子串是否是回文串来决定是否是回文串
  • 动态规划的核心在于寻找状态转移方程从小到大计算简化
  • 先列后行的处理简直太妙了,简化计算过程,后面的计算直接调用前面的计算结果。
  • 时间复杂度O(N^2)其中 n 是字符串的长度。动态规划的状态总数为 O(N^2) ,对于每个状态,我们需要转移的时间为 O(1)
  • 空间复杂度O(N^2),二维数组N*N
    在这里插入图片描述

在这里插入图片描述

class Solution {
public:
    string longestPalindrome(string s) {
        //特别处理,字符串长度为0或者1时直接返回s
        int len=s.size();
        if(len<2)
            return s;

        //定义最大长度和下标起始点
        int maxlen=1;
        int begin=0;
        
        //初始化dp二维数组
        vector<vector<int>> dp(len, vector<int>(len,0));
        for(int i=0;i<len;i++)
        {
            dp[i][i]=1;
        }
        
        //动态规划-先列后行
        for(int j=1;j<len;j++)//先列
        {
            for(int i=0;i<j;i++)//后行
            {
                if(s[i]!=s[j])//如果左右端点不相等肯定不是回文串
                {
                    dp[i][j]=0;
                }
                else
                {
                    if(j-i<3)//长度为3或者2的时候肯定是回文串
                    {
                        dp[i][j]=1;
                    }
                    else//看除端点之外的子串
                    {
                        dp[i][j]=dp[i+1][j-1];
                    }
                }

                if(dp[i][j] && j-i+1>maxlen)
                {
                    begin=i;
                    maxlen=j-i+1;
                }
            }
        }

        return s.substr(begin,maxlen);
    }
};

官方做法二-中心扩散

总结:

  • **while (left >= 0 && right < s.size() && s[left] == s[right])**这三个条件left和right的限制一定要在前面,防止出现溢出的情况
  • 所有的回文字符串最根本的东西,要么是一个字符,要么是两个相同的字符。所以针对每一个i点依据这两种情况进行中心扩散,一直扩散到不相等的i和j出现或者到达数组边界。在此过程中每一次i中心扩散,记录左端点和右端点计算长度,这两种情况取最大的情况。遍历完成,应该是最大回文串。
  • 时间复杂度O(N^2),其中 n 是字符串的长度。长度为 1和 2的回文中心分别有 n 和 n-1个,每个回文中心最多会向外扩展 O(n)次
  • 空间复杂度O(1)
class Solution {
public:
    pair<int, int> expandAroundCenter(const string& s, int left, int right) {
        while (left >= 0 && right < s.size() && s[left] == s[right]) {
            --left;
            ++right;
        }
        return {left + 1, right - 1};
    }

    string longestPalindrome(string s) {
        int start = 0, end = 0;
        for (int i = 0; i < s.size(); ++i) {
            auto [left1, right1] = expandAroundCenter(s, i, i);
            auto [left2, right2] = expandAroundCenter(s, i, i + 1);
            if (right1 - left1 > end - start) {
                start = left1;
                end = right1;
            }
            if (right2 - left2 > end - start) {
                start = left2;
                end = right2;
            }
        }
        return s.substr(start, end - start + 1);
    }
};

3.翻转字符串中的单词

自己做法总结:

  • 双指针。begin指针用于从后往前寻找单词的首字符,end指针在begin指针的位置上向后一直到此单词结束。
  • C++中string.size()函数没有C语言中’\0’之说,故得到的是字符串的真实长度,最后一个下标应该是size()-1
  • 先看头指针寻找单词首字符当前字符不为空并且下标等于0 或者 当前字符不为空,前一个字符为空
  • 再看尾指针寻找单词尾字符当前字符不为空并且下标小于等于size()-1
  • 每一个单词读取完成之后就加一个空格,最后整个字符串完成之后删除空格。
  • 时间复杂度:O(n)。假设字符串长度为n,头指针应该扫描n次O(n)
  • 空间复杂度:O(n)。拷贝一个字符串
class Solution {
public:
    string reverseWords(string s) {
        string temp;
        //字符串长度
        int length=s.size();
        //从后往前扫描
        //双指针。begin指针从后向前寻找单词首字符,end指针从首字符依次向后用于字符复制
        int begin,end;
        for(int begin=length-1;begin>=0;begin--)
        {
            if((s[begin]!=' ' && begin==0)||(s[begin]!=' ' && s[begin-1]==' '))//单词的首字符
            {
                end=begin;
                while((s[end]!=' ')&&(end<=s.size()-1))//到达字符串末尾或者单词的尾字符
                {
                    temp.push_back(s[end++]);
                }
                temp.push_back(' ');
            }
        }
        //去一个末尾空字符
        temp.erase(temp.size()-1);
        return temp;
    }
};

官方做法之自己编写的翻转函数

总结:

  • 整个字符串进行一次大的翻转reverse(s.begin(),s.end());
  • 从前往后进行扫描
  • first指针寻找单词首字符,若找到后则end指针指向first指针并不断后移,将其字符前移至idx指针代表的新字符串。(两个指针指向原字符串的单词,新指针指向新建立的字符串)
  • 对新单词进行翻转
  • 去除冗余字符
  • 时间复杂度:O(N),字符串长度为n,进行一次扫描
  • 空间复杂度:O(1),没有额外开销
class Solution {
public:
    string reverseWords(string s) {
        //先对整个字符串进行一次大翻转
        reverse(s.begin(),s.end());
        //获取字符串的长度
        int length=s.size();
        //从前往后遍历
        //一个单词头指针,一个单词尾指针
        int start,end=0;
        //新字符串的指针
        int idx=0;
        for(start=0;start<length;start++)
        {
            //寻找不是空格的字符,也即单词的首字符
            if(s[start]!=' ')
            {
                //每一个单词后面跟的空格
                if(idx!=0)
                    s[idx++]=' ';
                //把每一个单词的字符前移复制
                end=start;
                while(end<length && s[end]!=' ')
                {
                    s[idx++]=s[end++];
                }                
                //对每一个单词进行一次翻转
                reverse(s.begin()+idx-(end-start),s.begin()+idx);
                //头指针移到尾指针位置
                start=end;
            }
        }
        //擦除后面的冗余字符
        s.erase(s.begin()+idx,s.end());

        return s;
    }
};

官方做法三-数据结构栈的使用

总结:

  • 通过使用数据结构栈,进出每一个单词实现解题。
  • 核心还是在于word单词的筛选。遇到空格则表示一个word的结束,可以入栈。最后一个单词单独处理。
  • 时间复杂度:O(N),字符串长度n
  • 空间复杂度:O(N),所有单词的存储长度n
  • 前面的去段首和段尾的算法值得借鉴
class Solution {
public:
    string reverseWords(string s) {
        int left=0;
        int right=s.size()-1;
        //去掉字符串前部空格
        while(s[left]==' ')
            left++;
        //去掉字符串尾部空格
        while(s[right]==' ')
            right--;

        stack<string> temp;
        string word;
        while(left<=right)//扫描
        {
            //word不空且遇到空格,说明读完一个单词
            if(word.size() && s[left]==' ')
            {
                temp.push(word);
                word="";
            }
            else if(s[left]!=' ')
            {
                word+=s[left];
            }
            left++;
        }
        temp.push(word);//最后一个单词没有空格需要单独处理

        string ans;
        while(!temp.empty())
        {
            //取栈首元素
            ans+=temp.top();
            //出栈
            temp.pop();
            if(!temp.empty())
                ans+=' ';
        }

        return ans;
    }
};

4.KMP算法的字符串匹配

个人思路总结:

  • 具阅读左神的程序员面试指南,加深了这一知识点的理解。
  • 核心是两个知识点:一是如何建立前缀后缀匹配表,二是如何让模式字符串与原字符串进行匹配
class Solution {
public:
    void buildMatch(string needle, int *match)
    {
        int M=needle.size();
        //字符串为1需要单独处理
        if(M==1)
            match[0]=-1;
        else
        {
            match[0]=-1;
            match[1]=0;
            int cn=0;
            int pos=2;
            while(pos<M)
            {
                //pos-1字符与cn字符相等,则match[pos]=match[pos-1]+1=cn+1
                if(needle[pos-1]==needle[cn])
                {
                    match[pos++]=++cn;
                }
                else if(cn>0)
                {
                    cn=match[cn];
                }
                else
                {
                    match[pos++]=0;
                }
            }
        }
    }

    int strStr(string haystack, string needle) {
        //获取字符串的长度
        int N=haystack.size();
        int M=needle.size();
        //模式串为空返回0,模式串长度大于原字符串则返回-1
        if(M==0)
            return 0;
        if(M>N)
            return -1;
        //模式串的前后缀匹配表建立
        int *match=(int *)malloc(sizeof(int)*M);
        buildMatch(needle,match);
        //模式串与原字符串的匹配过程
        int n=0,m=0;
        while(n<N && m<M)
        {
            //匹配成功,下标则一同进步
            if(haystack[n]==needle[m])
            {
                n++;
                m++;
            }
            //匹配不成功,模式串需要转到match[]位置
            else if(m>0)
            {
                m=match[m];
            }
            //匹配不成功,模式串下标且已经退回到模式串0地址
            else
            {
                n++;
            }
        }
        //释放动态数组
        free(match);
        //依据模式串是否走到末尾决定返回下标
        return m==M ? n-m : -1;
    }
};

第四部分-双指针技巧

1.反转字符串
时间复杂度:O(N)。N/2次交换
空间复杂度:O(1)

class Solution {
public:
    void reverseString(vector<char>& s) {
        int begin=0,end=s.size()-1;
        while(begin<end)
        {
            swap(s[begin],s[end]);
            begin++;
            end--;
        }
    }
};

2.数组拆分I

个人思路总结

  • sort函数总结。sort(a,a+10)两个参数是从小到大排序,从大到小排序是三个参数sort(a,a+10, compare);
  • 为什么可以用排序做呢?可以这么理解,最小的元素只有和除它以外最小的元素进行组合,才不会影响较大的元素成为min(),这样总和就是最大的。
  • 时间复杂度:O(NlogN)。排序需要O(NlogN),遍历需要O(N)
  • 空间复杂度:O(1)。
class Solution {
public:
    int arrayPairSum(vector<int>& nums) {
        int n=nums.size();
        sort(nums.begin(),nums.end());
        int sum=0;
        for(int i=0;i<n;i+=2)
        {
            sum+=nums[i];
        }
        return sum;
    }
};

官方哈希表做法
总结:

  • 建立哈希表,以空间换取时间。
  • 关键点在于找好对应关系和累加值确定
  • 累加值确定的过程中d十分巧妙。计算d的过程中需要加2避免出现负数,计算i+10000的个数的时候(+1-d)/2向上取整
  • 时间复杂度:O(N)。哈希表扫描O(n)
  • 空间复杂度:O(N)。哈希表的空间为O(n)
class Solution {
public:
    int arrayPairSum(vector<int>& nums) {
        //建立哈希表初始化
        int arr[20001]={0};
        //i元素的个数存放在arr[i+10000]位置
        for(int i=0;i<nums.size();i++)
        {
            arr[nums[i]+10000]++;
        }
        //扫描哈希表
        int d=0;
        int sum=0;
        for(int i=0;i<20001;i++)
        {
            sum+=(arr[i]+1-d)/2*(i-10000);//除2向上取整
            d=(2+arr[i]-d)%2;//避免出现负数
        }
        return sum;
    }
};

3.输入有序数组

这道题可以使用 两数之和 的解法,使用 O(n^2)的时间复杂度和 O(1) 的空间复杂度暴力求解,或者借助哈希表使用O(n) 的时间复杂度和 O(n) 的空间复杂度求解。但是这两种解法都是针对无序数组的,没有利用到输入数组有序的性质。利用输入数组有序的性质,可以得到时间复杂度和空间复杂度更优的解法

总结:个人做法没有A过,超出时间限制

class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        vector<int> temp;
        for(int i=0;i<numbers.size()-1;i++)
        {
            for(int j=i+1;j<numbers.size();j++)
            {
                if(numbers[i]+numbers[j]==target)
                {
                    temp.push_back(i+1);
                    temp.push_back(j+1);
                    break;
                }
            }
            if(temp.size())
                break;
        }
        return temp;
    }
};

官方做法一双指针

总结

  • 双指针真的非常爽了,有效降低了时间复杂度。一个指向头部,一个指向尾部。
  • 时间复杂度:O(N)。字符串长度N
  • 空间复杂度:O(1)。常数
class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        int low=0,high=numbers.size()-1;
        int sum=0;
        while(low<high)
        {
            sum=numbers[low]+numbers[high];
            if(sum==target)
            {
                return {low+1,high+1};
            }
            else if(sum>target)
            {
                high--;
            }
            else
            {
                low++;
            }
        }
        return {-1,-1};
    }
};

个人做法-二分查找

可以看看本人总结的二分查找模板

https://blog.csdn.net/qq_39309050/article/details/109097745

总结

  • 这种sum和的形式肯定可以拆分成一个已知和一个查找的问题。
  • 熟练使用应用二分查找的模板
  • 时间复杂度:O(N logN)。其中O(N)的遍历第一个元素,O(logN)的二分查找,两者相乘
  • 空间复杂度:O(1)
class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        for(int i=0;i<numbers.size()-1;i++)
        {
            int findnum=target-numbers[i];
            int low=i+1;
            int high=numbers.size();//左闭右开
            int mid=0;
            //二分查找
            while(low<high)
            {
                mid=low+(high-low)/2;

                if(findnum>numbers[mid])
                {
                    low=mid+1;
                }
                else
                {
                    high=mid;
                }
            }
            if(low==numbers.size())//如果最后找到的下标是右端点,则失效
                continue;
            if(numbers[low]==findnum)//判断找到的元素是否正确
                return {i+1,low+1};
        }
        return {-1,-1};
    }
};

使用了大佬的模板之后

https://blog.csdn.net/qq_39309050/article/details/109101552

class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        for(int i=0;i<numbers.size()-1;i++)
        {
            int findnum=target-numbers[i];
            int low=i+1;
            int high=numbers.size()-1;//左闭右开
            int mid=0;
            //二分查找
            while(low<=high)
            {
                mid=low+(high-low)/2;

                if(findnum>numbers[mid])
                {
                    low=mid+1;
                }
                else if(findnum<numbers[mid])
                {
                    high=mid-1;
                }
                else if(findnum==numbers[mid])
                    return {i+1,mid+1};
            }

        }
        return {-1,-1};
    }
};

4.双指针之移除元素

总结:

  • 理解了双指针,此题相当容易。slow慢指针只有在fast快指针指向的元素不等于目标值的时候,才进行被替换和slow指针后移。
    时间复杂度:O(N)。假设数组总共有 n个元素,i 和 j 至多遍历 2n 步
    空间复杂度: O(1)
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int slow=0;
        int len=nums.size();
        for(int fast=0;fast<len;fast++)
        {
            if(nums[fast]!=val)
                nums[slow++]=nums[fast];
        }

        return slow;
    }
};

比较喜欢官方说的那个适用于删除较少元素的双指针操作

  • 时间复杂度 :O(N)。最多n步
  • 空间复杂度 : O(1)
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int len=nums.size();
        int fast=0;
        while(fast<len)
        {
            if(nums[fast]==val)
            {
                nums[fast]=nums[len-1];
                len--;
            }
            else
            {
                fast++;
            }
        }
        return len;
    }
};

5.最大连续1的个数

总结

  • 快慢指针的基本套路
  • 注意点:最后一次计算的slow值要跟maxslow值做一次比较
  • 时间复杂度:O(N)
    -空间复杂度:O(1)
class Solution {
public:
    int findMaxConsecutiveOnes(vector<int>& nums) {
        int slow = 0;
        int maxslow = 0;
        for(int i = 0; i < nums.size(); i++)
        {
            if(nums[i]!=1)
            {
                if(slow > maxslow)
                {
                    maxslow = slow;
                }
                slow = 0;
            }
            else
            {
                slow++;
            }
        }
        if(slow > maxslow)
        {
            maxslow = slow;
        }
        return maxslow;
    }
};

6.长度最小的子数组

总结:

  • 慢指针用于从前往后扫描数组,快指针用于计算从慢指针位置向后扫描。
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        int len = nums.size();
        if(len == 0)
            return 0;
        long minlen = 1410065407;
        long sum = 0;
        int fast = 0;
        int slow = 0;
        int i = 0;
            for(int fast = 0; fast < len; fast++)
            {
                sum += nums[fast];
                if(sum >= s)
                {
                    if(fast - slow + 1 < minlen)
                    {
                        minlen = fast - slow + 1;
                    }
                    sum = 0;
                    fast = slow++;
                }
                //cout << "sum:" << sum << "minlen:" << minlen << endl;
            }

        if(minlen == 1410065407)
            return 0;
        return minlen;
    }
};

官方一:暴力解法
总结:

  • INT_MAX的用法
//暴力解法
class Solution {
public:
    int min(int a, int b)
    {
        return a > b ? b : a;
    }
    int minSubArrayLen(int s, vector<int>& nums) {
        int len = nums.size();
        if(len == 0)
            return 0;
        
        int sum = 0;
        int minlen = INT_MAX;
        for(int i = 0; i < len; i++)
        {
            sum = 0;
            for(int j = i; j < len; j++)
            {
                sum += nums[j];
                if (sum >= s)
                {
                    minlen = min (minlen, j - i + 1);
                    break;
                }
            }
        }

        return minlen == INT_MAX ? 0 : minlen;
    }
};

官方二:前缀和+二分 解法
总结:

  • 这种做法我只能说很恶心了,主要是那个下标那里弄的傻傻的分不清。
  • 假设原数组长度为len,即nums[0],nums[1]……nums[len-1]
  • 建立len+1个元素的sum[]数组,其中sum[i]表示前i个元素的和
  • 二分查找里面右端点是设置为len+1,也即最后查找结果如果是len+1则说明不符合要求。
class Solution {
public:
    int min(int a, int b)
    {
        return a > b ? b : a;
    }
    int twoDivid(vector<int>& sum, int target)
    {
        int left = 0;
        int right = sum.size();
        while(left < right)
        {
            int mid = left + (right - left) / 2;
            if(sum[mid] < target)
            {
                left = mid + 1;
            }
            else
            {
                right = mid;
            }
        }
        return left;
    }

    int minSubArrayLen(int s, vector<int>& nums) {
        int len = nums.size();
        if(len == 0)
            return 0;

        vector<int> sum(len + 1, 0);
        //sum[i]存放[0]+[1]……+[i-1]
        //sum[i]表示前i个元素和
        for(int i = 1; i <= len; i++)
        {
            sum[i] = sum[i - 1] + nums[i - 1];
            cout << "sum[%d]-"<< i << " "<<sum[i] <<endl; 
        }

        int target = 0;
        int bound = 0;
        int minlen = INT_MAX;
        for(int i = 1; i <= len + 1; i++)
        {
            target = sum[i - 1] + s;
            bound = twoDivid(sum, target);
            //auto bound = lower_bound(sum.begin(), sum.end(), target);            
            if (bound != sum.size())
                minlen = min(bound - i + 1,minlen);
        }

        return minlen == INT_MAX ? 0: minlen;
    }
};

官方的可取之处

  • lower_bound 函数的使用
  • bound的类型不是int,需要特殊处理
        for (int i = 1; i <= n; i++) {
            int target = s + sums[i - 1];
            auto bound = lower_bound(sums.begin(), sums.end(), target);
            if (bound != sums.end()) {
                ans = min(ans, static_cast<int>((bound - sums.begin()) - (i - 1)));
            }
        }

class Solution {
public:
    int min(int a,int b)
    {
        return a<b ? a:b;
    }
    int minSubArrayLen(int s, vector<int>& nums) {
        int len = nums.size();
        if(len==0)
            return 0;
        int start = 0;
        int end = 0;
        int sum = 0;
        int minlen = INT_MAX;
        while(end < len)
        {
            sum += nums[end];     
            while(sum >= s)
            {
                minlen = min(end - start + 1, minlen);
                sum -= nums[start++];
            }
            end++;
        }
        return minlen==INT_MAX ? 0 : minlen;
    }
};

官方双指针做法
总结:

  • 两个指针,start指针指向最小数组的首位,end指针指向最小数组的尾位,初始值都是0
  • end指针不断地后移,一直到sum和大于s.循环:记录此时的长度,start指针后移,sum和减去首位元素值,一直到sum和小于s退出循环……

class Solution {
public:
    int min(int a,int b)
    {
        return a<b ? a:b;
    }
    int minSubArrayLen(int s, vector<int>& nums) {
        int len = nums.size();
        if(len==0)
            return 0;
        int start = 0;
        int end = 0;
        int sum = 0;
        int minlen = INT_MAX;
        while(end < len)
        {
            sum += nums[end];     
            while(sum >= s)
            {
                minlen = min(end - start + 1, minlen);
                sum -= nums[start++];
            }
            end++;
        }
        return minlen==INT_MAX ? 0 : minlen;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值