LeetCode刷题记录(小森要找工作啦!!!!!!!!!!)

3.给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

  • 示例 1:
    输入: "abcabcbb"
    输出: 3
    解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
  • 示例 2:
    输入: "bbbbb"
    输出: 1
    解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
  • 示例 3:
    输入: "pwwkew"
    输出: 3
    解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。

拿到这个题目首先的想法是建立一个滑动窗口,利用两个变量来维护,之后对字符串进行遍历,一边遍历一边对滑动窗口内的所有字符进行检测是否重复,如果没有重复则窗口右侧增加一位,如果有重复,则滑动窗口左侧指针更新为窗口内重复元素的位置+1。代码如下:

#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;

int main(int argc, char const *argv[])
{
    string s = "pwwkew";  //样例信息
    int l, ans;      //利用l和i维持一个滑动窗口,窗口范围是[l,i)
    l = 0;    
    ans = 0;
    string s_temp;

    for (int i = 0; i < s.length(); ++i)
    {
        s_temp = s.substr(l,i-l);    // s.substr(pos,n);  从第pos个位置开始(包括pos),返回n个字符;
        for(int j = 0; j < s_temp.length(); j++){    //从滑动窗口内开始遍历,如果串口内存在与正在扫描的第i个字符相同,则改变l指针
            if( (s_temp[j] == s[i])  && (i != 0) ){
                l = l + j + 1;                //把l指针位置改变为:在窗口内发现的重复字符的后一个。
            }
        }
        if(  (i - l + 1) >= ans ){         //更新最大滑动窗口长度;
            ans  =  (i - l + 1);
        }
    }
};
    }
      
    cout << ans << endl;
    return 0;
}

提交结果时间复杂度太高 \(O(n^2)\) ,后来学习了hash_map,可以利用hash—map来记录每个字符串出现的位置,这样就不需要对滑动窗口的每个元素进行遍历了,而且hash_map的复杂度是\(O(1)\),所以最后的复杂度是 \(O(n)\) 。代码更改如下:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int res = 0, left = -1, n = s.size();
        unordered_map<int, int> m;
        for (int i = 0; i < n; ++i) {
            if (m.count(s[i]) && m[s[i]] > left) 
                left = m[s[i]];  
            m[s[i]] = i;
            res = max(res, i - left);            
        }
        return res;
    }
};

5.最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

  • 示例 1:
    输入: "babad"
    输出: "bab"
    注意: "aba" 也是一个有效答案。
  • 示例 2:
    输入: "cbbd"
    输出: "bb"

这个题目,也很简单,分两种情况依次遍历,分别找出奇数和偶数长度的子串就可以了,注意数组越界问题(找了挺长时间哪里数组越界,看来对编程还是不敏感),此处在search函数里left和right指针在进行判断之前,已经是不符合判断条件的状况了,所以我们如果后续会利用这两个变量,需要对其进行复原,即left++right--(就是这里找了好久,~~(>_<)~~),代码如下:

class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.length();
        if(n< 2)
            return s;
        int start = 0;
        int maxLen = 0;
        for(int i = 0; i < s.length();i++)  //寻找长度为奇数
            search(s, i,  i,  start, maxLen);
        for(int i = 0; i < s.length()-1;i++)  //寻找长度为偶数
            search(s, i, i+1, start, maxLen);
        return s.substr(start, maxLen);
    }
    
    void search(string s,int left, int right, int& start,int& maxLen){
        while( (left >= 0) && (right < s.length()) && (s[left] == s[right])    ){
            left--;
            right++;
        }
        left++;
        right--;
        if( (right - left + 1) > maxLen  ){
            maxLen = right - left + 1;
            start = left;
        }
    }
};

今天尝试了用动态规划的方法去解题,动态规划是一个很像分治法的算法,都是“大事化小小事化了”的思想。动态规划实际上就是依次推这个公式:\(F(n) = F(n-1)+F(n-2)\),其中\(F(n)\)使我们想要的最终状态,当然我们需要边界条件,就是\(F(0)=a\)\(F(1)=b\),因为到了这里我们就不能继续利用上面的地推公式了,要在此收敛。当然我们可以正着推,也可以反过来递归求解,但是递归求解相当于对一颗二叉树的所有节点进行求解,故时间复杂度是\(O(2^n)\),所以一般从边界条件开始正着推,此时也可以利用备忘录算法,把已经得到的数值放到map或者数组里,避免重复求解。

这个题目里我利用了mark[i][j]这个数组(我们只利用 i < j 的这一部分),i就是子串的左边界,j就是子串的右边界,如果\(s[i,j]\)这个子串是一个回文子串那么mark[i][j] = 1。判断其是否是回文子串的条件是动态规划算法的核心if( (s[i] == s[j]) && ( j-i<2 || mark[i+1][j-1] ) ),其边界条件是mark[i][i]=1。除此之外,需要注意的是,因为我们是从边界条件开始递推所有数组的状态,所以我们需要沿着对角线方向依次向上遍历mark数组,以确保递推时候的正确性,图示如下:

class Solution {
public:
    string longestPalindrome(string s) {
        int start = 0;
        int n = s.length();
        if (n < 2 )
            return s;
        int mark[n][n] = {0};
        for(int i = 0; i < n ;i++)   //初始化数组
            for(int j = 0;j < n;j++)
                mark[i][j]=0;
        int MaxLen = 0;
        int left = 0;
        
        for(int k = 0; k < n; k++){
            for(int i = 0,j = k; i < n - k; i++,j++){          //确保沿对角线向数组右上方遍历;
                mark[i][i] = 1;
                if( (s[i] == s[j])  && ( ( j-i<2 ) || mark[i+1][j-1] ) )
                    mark[i][j] = 1;
                if( mark[i][j] && (j-i+1 > MaxLen) ) {
                    MaxLen = j-i+1;
                    start = i;
                }
            }
        }
        return s.substr(start,MaxLen);
    }
};

实际提交后,会发现这种算法的时间复杂度实际很不理想,这和遍历数组的方式有关。数组在计算机中是按行线性排列的,而且计算机在从内存或缓存加载数据的时候,会默认把当前访问的数据以及其附近的多组数据一同加载,这样可以提高下次访问的速度(计算机会猜测你下次极有可能访问临近的元素),然后我们的这种访问方式就使得计算机的这种优化无用了,所以我们每次访问数组中的元素,计算机都会从新加载数据,进而时间复杂度极其不理想。
刚才上网看还有一个马拉车算法,今天还有事情,明天看一看回来更!码住

6.Z字形变换

将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。

比如输入字符串为 "LEETCODEISHIRING" 行数为 3 时,排列如下:

L   C   I   R
E T O E S I I G
E   D   H   N

之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"LCIRETOESIIGEDHN"。

请你实现这个将字符串进行指定行数变换的函数:

string convert(string s, int numRows);

  • 示例 1:
    输入: s = "LEETCODEISHIRING", numRows = 3
    输出: "LCIRETOESIIGEDHN"
  • 示例 2:
    输入: s = "LEETCODEISHIRING", numRows = 4
    输出: "LDREOEIIECIHNTSG"
    解释:
L     D     R
E   O E   I I
E C   I H   N
T     S     G

这个题目比较简单,很容易发现规律:对于每n排竖直排列的字母,相差的索引都是size = 2 * ,numRows - 2个。随后我们判断是否是首行或者尾行,如果是,直接利用上面规律就好,如果不是,我们需要插入 s[j + size - 2 * i]个元素(i是当前行数,j是一个循环变量变量,j每次递增size个单位)。此外需要注意数组是否越界,和输入空字符串的情况。

class Solution {
public:
    string convert(string s, int numRows) {
        
        if (numRows <= 1) return s;
        string res = "";
        int size = 2 * numRows - 2;

        for (int i =0; i< numRows; i++){
            for(int j = i; j < s.length(); j = j + size){
                if( (i == 0)||(i==numRows-1) ){ //说明是首行或者尾行
                    res += s[j];
                }else{  //中间行
                    res += s[j];
                    if( j + size - 2 * i < s.length() ) //边界判断
                        res += s[j + size - 2 * i];
                }
            }
        }
        return res;
    }
};

12.整数转罗马数字

罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。

字符 数值
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做  XXVII, 即为 XX + V + II 。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。 
C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。
给定一个整数,将其转为罗马数字。输入确保在 1 到 3999 的范围内。

  • 示例 3:
    输入: 9
    输出: "IX"

  • 示例 4:
    输入: 58
    输出: "LVIII"
    解释: L = 50, V = 5, III = 3.

  • 示例 5:
    输入: 1994
    输出: "MCMXCIV"
    解释: M = 1000, CM = 900, XC = 90, IV = 4.

这个题目拿过来第一反应是要把每个位数分离出来,随后转换成罗马数字,再拼接到一起。中间利用了map:

sclass Solution {
public:
   string intToRoman(int num) {
       string  ans = "";
       int t = 10;
       int temp = 0;
       
       while( num != 0  ) {
           temp = num % t;
           num -= temp;
           t *= 10;
          ans = getOneRoman(temp) + ans;
       }
       return ans; 
   }
   
   string getOneRoman(int num){
       map<int,string> mapRom = {{1,"I"},{4,"IV"},{5,"V"},{9,"IX"},{10,"X"},{40,"XL"},{50,"L"}, {90,"XC"},{100,"C"},  {400,"CD"},{500,"D"},{900,"CM"}, {1000,"M"} };
       map<int,string>::reverse_iterator  it;
       string OneStr = "";
       while(num != 0){
           for(it = mapRom.rbegin(); it != mapRom.rend(); it++ ) {//从后向前遍历
               if(it->first <= num ){
                   num -= it->first;
                   OneStr += it->second;
                   break;
               }
           }
       }
       return OneStr;
   }
};

提交之后意识到时间复杂度惨不忍睹(一个数字长度是n,那么这个方式\(n^k\))。。。。。。。
其实何必把每个位数单独分离出来呢?我们从大向小遍历,转换一个位数后,把基数减去相应的位数,继续在map中寻找。这样在map中也只需要遍历一遍。

class Solution {
public:
   string intToRoman(int num) {
       string  ans = "";
       vector<int> values={1000,900,500,400,100,90,50,40,10,9,5,4,1}; 
       vector<string> strs={"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"};
       int i = 0;
       while( num != 0 || i > values.size() ){
           if(  num >= values[i]){
               num -= values[i];
               ans += strs[i];
           }else
               i++;
       }
       return ans; 
   }
};

猜你喜欢

转载自www.cnblogs.com/JeasonIsCoding/p/11626836.html