前言
今年要准备夏令营机考,因此决定在leetcode上刷题。但根据自己以前的经验,往往刷了题之后就不再去回看,导致某些题目仍然会遗忘,因此决定边刷题边用博客记录下来,加深印象!我的题目是leetcode中文网站上的,关于题目具体描述和输入输出可以参考(https://leetcode-cn.com/problemset/all/)。代码都是自己写的,也许不是最巧妙的,但是是自己觉得最好理解的解题方式。话不多说,开始刷题!
1、两数之和
题目描述:
给一个整数数组,找出其中和为某个特定值的两个数。 |
题目虽然不难,但还是有几种方法可以解决!
解题思路
思路一
暴力搜寻,用两次循环来实现,时间代价是 |
代码如下
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int s = nums.size();
vector<int> res;
for(int i=0;i<s-1;i++){
for(int j = i+1;j<s;j++){
if(nums[i] + nums[j] == target){
res.push_back(i);
res.push_back(j);
}
}
}
return res;
}
};
思路二
第二种方法是先对数组进行排序,然后用两个指针head和tail,一个从头开始,一个从尾巴开始,每次 的值与target的关系,如果相等直接就得到结果,如果和大于target,tail就往前走,如果小于target,head就往后走,这样只需遍历一遍,总的时间代价是排序的 。由于输出要求是两个数的在数组中的位置,可以定义一个数据结构来存储每个值和它对应的位置 |
代码如下
class Solution {
private:
struct node{
int val;
int ind;
node(int v,int i){
val = v;
ind = i;
}
bool operator <(const node & other){
return this->val < other.val;
}
};
public:
vector<int> twoSum(vector<int>& nums, int target) {
int len = nums.size();
vector<node> sort_nums;
for(int i=0;i<len;i++){
sort_nums.push_back(node(nums[i],i));
}
sort(sort_nums.begin(),sort_nums.end());
int head = 0, tail = len-1;
vector<int> res;
while(head<tail){
int tmp = sort_nums[head].val+sort_nums[tail].val;
if(tmp == target){
res.push_back(sort_nums[head].ind);
res.push_back(sort_nums[tail].ind);
return res;
}
if(tmp>target){
tail -= 1;
}
if(tmp<target){
head += 1;
}
}
}
};
第三种思路
利用hash表来存储每个值所需要的值,这种方法很巧妙,利用了hashmap查询时间为 的特性。c++中的hash表是STL中的unordered_map |
代码如下
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int len = nums.size();
unordered_map<int,int> hashMap;
int tmp;
for(auto i=0;i<len;i++){
tmp = target - nums[i];
unordered_map<int,int>::iterator iter = hashMap.find(tmp);
//找到了对应的值
if(iter != hashMap.end()){
return vector<int>({iter->second,i});
}
hashMap[nums[i]] = i;
}
}
};
2、两数相加
题目描述:
给定两个非空单列表,求其和,返回也是一个但列表,对数字的存储是逆序的。题目不难,主要 是列表的操作需要细心,我自己实现的代码如下: |
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
int carry = 0;
int sum = l1 -> val + l2 -> val + carry;
ListNode * head = new ListNode(sum % 10);
ListNode * tmp = head;
carry = sum / 10;
l1 = l1 -> next;
l2 = l2 -> next;
int val1 = 0;
int val2 = 0;
//只要有一个不为null就继续进行下去
while(l1 || l2 || carry){
//此处l1和l2如果为NULL就保持仍为NULL
val1 = l1 ? l1 -> val : 0;
val2 = l2 ? l2 -> val : 0;
sum = val1 + val2 + carry;
tmp -> next = new ListNode(sum % 10);
carry = sum / 10;
l1 = l1 ? l1 -> next : NULL;
l2 = l2 ? l2 -> next : NULL;
tmp = tmp -> next;
}
return head;
}
};
3、无重复字符最长子串
题目描述
给一个字符串,找出不含有重复字符的子串长度,例如abcabbca的最长无重复子串就是abc和
bca、cab,其长度均是3,所以最长子串长度是3。
解题思路
思路一
最简单的方法当然是直接暴力(这也是找不到好的解决办法的时候的一个不错的选择,可以得一
部分分),这题的暴力是枚举所有的子串,然后判断是否有重复字符,这种方法时间代价是
$O(N^3)$,往往是不能通过全部测试样例的
代码如下:
class Solution {
private:
bool isLegal(const string &subStr){
unordered_map<char,int> temp;
unordered_map<char,int>::iterator iter;
for(int i = 0;i<subStr.length();i++){
iter = temp.find(subStr[i]);
//有重复字符
if(iter != temp.end()){
return false;
}
temp[subStr[i]] = 0;
}
return true;
}
public:
int lengthOfLongestSubstring(string s) {
string subStr;
int len = s.length();
if(s == "") return 0;
if(len == 1) return 1;
int max = 1;
for(int i = 0; i < len ; i++){
int leLen = len - i ;
for(int j = 1;j <= leLen; j++){
subStr = s.substr(i,j);
if(isLegal(subStr)){
max = max > j ? max : j;
}
}
}
return max;
}
};
运行结果是有两个样例通不过,看了一下发现没通过的测试样例也不是很长,因此需要时间复杂度更小的方法
思路二
在从 到 的子串中,如果这个子串已经是证明没有重复字符的,那么我们就可以把 这个字符加入到子串中,这个新的子串也是没有重复字符的,但如果s[j]已经是在子串中,假设 ,那么就可以直接把i变成 ,因为如果 不跳过 的话,从i到后面的 仍会有重复的字符。这个从 到 构成了一个窗口,窗口内的就是没有重复字符的子串 |
代码如下
class Solution {
private:
//找到字符的位置
int findChar(string subStr, char c){
for(auto i=0;i<subStr.length();i++){
if(subStr[i] == c){
return i;
}
}
return -1;
}
public:
int lengthOfLongestSubstring(string s) {
if(s == "") return 0;
string subStr;
int len = s.length();
if(len == 1) return 1;
int i =0, j = 1;
int max = 1;
while(i < len && j < len){
subStr = s.substr(i,j-i);
int index = findChar(subStr,s[j]);
if(index == -1){
if(j+1 == len){
return max > j - i + 1 ? max : j - i + 1;
}else{
j = j + 1;
}
}else{
max = max > j - i ? max : j -i;
i = i + index + 1;
}
}
}
};
这种方法可以通过样例,但是还可以改进,主要是查询效率不高,如果能够维护一个窗口的hash表就更好了,将字符的位置存为value,这样查询和找位置也方便很多。先判断是否有s[j]是否在窗口里面,在的话,直接取出value值即可。
代码如下
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if (s == "") return 0;
int len = s.length();
if (len == 1) return 1;
int i = 0, j = 1;
int max = 1;
unordered_map<char, int> set;
unordered_map<char, int>::iterator iter;
set[s[i]] = i;
while (i < len && j < len) {
iter = set.find(s[j]);
//s[j]不在子串中
if (iter == set.end()) {
if (j + 1 == len) {
return max > j - i + 1 ? max : j - i + 1;
}
else {
set[s[j]] = j;
j = j + 1;
}
}
else {
int index = iter->second;
max = max > j - i ? max : j - i;
for (int k = i; k < index + 1; k++) {
set.erase(set.find(s[k]));
}
i = index + 1;
}
}
return max;
}
};
4、两个排序数组的中位数
题目描述
给两个规模为m和n的有序数组,求这两个数组的中位数,要求时间复杂度为O(log(m+n)),这个 题稍微有点复杂,先放这儿,下次再说。。。 |
5、最长回文子串
题目描述
找出某个字符串中的最长回文子串。 |
第一种方法当然是暴力解决,没啥好说的。
class Solution {
private:
bool isLegal(string s) {
int len = s.length();
for (int i = 0; i<len / 2; i++) {
if (s[i] != s[len - 1 - i]) return false;
}
return true;
}
public:
string longestPalindrome(string s) {
int max = 1;
int len = s.length();
if(len == 1) return s;
string subStr;
char first = s[0];
char _res [] = {first,};
string res = string(_res);
for (int i = 0; i<len; i++)
//直接排除长度比max小的子串
for (int j = i + max; j<len; j++) {
subStr = s.substr(i, j - i + 1);
if (isLegal(subStr)) {
max = max >= j - i + 1 ? max : j - i + 1;
res = subStr;
}
}
return res;
}
};
虽然过了测试样例,但毕竟耗时太长,复杂度有点高,为 。下面继续探讨
回文串的特点是左右对称的,因此我们可以考虑从某个字符串进行扩展生成回文子串,如对aba 左右各加一个c,就构成了新的子串cabac。但是子串的长度可以为偶数,也可以为奇数,因此 在基础串长度可以为1也可以为2,故而需要分开讨论。这种方法的时间复杂度为 。 |
代码如下
class Solution {
public:
string longestPalindrome(string s) {
if (s == "") return "";
int len = s.length();
if (len == 1)return s;
int max = 1;
int i, j;
string res = string({ s[0], });
//基础串长度为1
for (int k = 1; k<len - 1; k++) {
i = k - 1;
j = k + 1;
while (i >= 0 && j < len) {
if (s[i] == s[j]) {
if ((j - i + 1) > max) {
res = s.substr(i, j - i + 1);
max = j - i + 1;
}
i -= 1;
j += 1;
}
else {
break;
}
}
}
//基础串长度为2
for (int k = 0; k<len - 1; k++) {
if (s[k] == s[k + 1]) {
res = max > 2 ? res : string({s[k],s[k+1]});
i = k - 1;
j = k + 2;
while (i >= 0 && j < len) {
if (s[i] == s[j]) {
if ((j - i + 1) > max) {
res = s.substr(i, j - i + 1);
max = j - i + 1;
}
i -= 1;
j += 1;
}
else break;
}
}
}
return res;
}
};
这种方法就快了很多,但仍然不是最快的解决办法,我看到网上还有一种 的解决办法,没细看,点击这里
6、z字形变换
题目描述
将一个字符串按照给定的 行数排列成Z字形,例如将”PAYPALISHIRING”进行三行排列,结果如下
P A H N
A P L S I I G
Y I R
然后从左往右读取字符,其结果就是”PAHNAPLSIIGYIR”,进行四行排列,结果就是
P I N
A L S I G
Y A H R
P I
结果是”PINALSIGYAHRPI”,题目给定字符串和行数,求这个输出的字符串
解题思路
我自己的想法是逐个计算每个字符在Z字形排列后的位置,因此先分析Z字形生成的规律。遍历原字符串,每numRows-1个字符之后,Z字形排列的方向发生改变。以行数为4为例,前三个数在第一条竖直线上,此时他们的纵坐标是一样的,都为零,而第4,5,6个字符则是行坐标递减,纵坐标递增,因此我们只需要用一个 dir 变量来表示当前变化的方向,用 r 和 c 来表示当前字符的坐标,并根据dir的值动态改变 r 和 c 的值。 |
代码如下:
class Solution {
public:
string convert(string s, const int numRows) {
if(numRows == 1) return s;
const int len = s.length();
char ** arr = new char *[numRows];
for (int i = 0; i < numRows; i++) {
arr[i] = new char[len];
for (int j = 0; j < len; j++)
arr[i][j] = '0';
}
//dir==0表示竖直方向,dir==1表示斜线方向
//r — 横坐标, c — 纵坐标
int r = 0, dir = 0, c = 0;
for (int i = 0; i<len; i++) {
if (!dir) {
arr[r][c] = s[i];
if (r == numRows - 1) {
dir = 1;
r -= 1;
c += 1;
}
else r++;
}
else {
arr[r][c] = s[i];
if (r == 0) {
dir = 0;
r += 1;
}
else {
c++;
r--;
}
}
}
string res = "";
for (int i = 0; i<numRows; i++)
for (int j = 0; j<len; j++) {
if (arr[i][j] != '0') res += arr[i][j];
}
return res;
}
};
行数为1时是特殊情况,直接返回原字符串即可。另外对拐角处的字符坐标容易出错。这种方法容易理解,但复杂度为 ,空间复杂度为 ,其中 是字符串的长度
7、反转整数
题目描述
给一个 型整数,求出该整数的反转之后的数字,如123反转之后为321,-123反转之后为-321,如果反转后数字不在整数范围内 范围内,则返回零 |
解题思路
题目比较简单,求出每一位,逆序计算即可,主要是要考虑翻转后数字的范围问题,因此可用 类型来存储数字,便于比较,计算指数用的是 库里面的 函数 |
代码如下
class Solution {
public:
int reverse(int x) {
if (x == 0) return 0;
long long max = pow(2, 31) - 1;
long long min = -pow(2, 31);
int left, mode;
vector<int> temp;
long long res = 0;
long long carry = 1;
if (x > 0) {
left = x / 10;
mode = x % 10;
while (left > 0 || mode > 0) {
temp.push_back(mode);
mode = left % 10;
left /= 10;
}
for (int i = temp.size() - 1; i >= 0; --i) {
res += temp[i] * carry;
carry *= 10;
}
if (res > max) return 0;
return res;
}
else {
left = -x / 10;
mode = -x % 10;
while (left >0 || mode > 0) {
temp.push_back(mode);
mode = left % 10;
left /= 10;
}
for (int i = temp.size() - 1; i >= 0; i--) {
res += temp[i] * carry;
carry *= 10;
}
if (-res < min) return 0;
return -res;
}
}
};
8、字符串转整数
题目描述
将字符串转为整数,或从字符串里面提取出整数,要求第一个非空字符必须为+,-,或者数字,否则无法提取。 |
解题思路
这题不难,主要在于对字符串的多种形式需要考虑完备。比如”+-131wera”就当作无法提取,还有考虑全零的数如”000000”以及”+000321”等类型的字符串类型,感觉就是比较麻烦,对算法没什么要求,总体来看题目没啥意义。 |
代码如下
class Solution {
private:
bool isDigit(char c) {
return c >= '0' && c <= '9';
}
public:
int myAtoi(string str) {
int len = str.length();
vector<char> res;
//findNum判断是否可以匹配
bool findNum = false;
const long long MAX = pow(2, 31) - 1;
const long long MIN = -pow(2, 31);
for (int i = 0; i<len; i++) {
if (!findNum && str[i] != ' ' && !isDigit(str[i]) && str[i] != '-' && str[i] != '+') return 0;
if (findNum && !isDigit(str[i])) break;
if (isDigit(str[i])) {
findNum = true;
res.push_back(str[i]);
}
if ((str[i] == '-' || str[i] == '+') && !findNum) {
findNum = true;
res.push_back(str[i]);
}
}
if (res.empty()) return 0;
long long temp = 0;
long long carry = 1;
if (isDigit(res[0])) {
int begin = -1;
//对于可能第一位为0的数字,需要去除掉
for (int i = 0; i < res.size(); i++) {
if (res[i] != '0') {
begin = i;
break;
}
}
if (begin == -1) return 0;
if (res.size() - begin > 10) return MAX;
for (int i = res.size() - 1; i >= begin; --i) {
temp += (res[i] - '0') * carry;
carry *= 10;
}
if (temp > MAX) return MAX;
return temp;
}
else {
int begin = -1;
//对于可能第一位为0的数字,需要去除掉
for (int i = 1; i < res.size(); i++) {
if (res[i] != '0') {
begin = i;
break;
}
}
if (begin == -1) return 0;
if (res.size() - begin > 11) return res[0] == '+' ? MAX : MIN;
carry = 1;
for (int i = res.size() - 1; i >= begin; --i) {
temp += (res[i] - '0') * carry;
carry *= 10;
}
if (res[0] == '+') return temp > MAX ? MAX : temp;
return -temp < MIN ? MIN : -temp;
}
}
};
10、回文数
题目说明
判断一个数是否是回文数 |
解题思路
首先最简单的方法是用最基本的判断方法:即回文是对称的。因此可逐位求出个位数,然后判断是否对称。这种方法所用时间复杂度为 ,空间复杂度也为 ,应该算是一种不错的方法。此外可以排除一些基本的情况,即负数直接返回false,小于十的数直接返回true。 |
class Solution {
public:
bool isPalindrome(int x) {
if(x < 0) return false;
if(x < 0) return true;
int carry = 1;
vector<int> temp;
int left = x / 10;
int mode = x % 10;
if(left == 0) return true;
temp.push_back(mode);
while(left > 0){
mode = left % 10;
left /= 10;
temp.push_back(mode);
}
int i = 0, j = temp.size() - 1;
while(i < j){
if(temp[i] != temp[j]) return false;
i++;
j--;
}
return true;
}
};
但是我看到网上有种空间复杂度更低的方法,即是通过反转一半的数字进行判断。比如123321我们从最右边开始,一次求出其对应位的数字,1 , 2, 3,然后计算成新的数字321与原数字移位之后剩下的数字(321)进行比较即可。关键是如何找到原数字的一半位数,可通过比较移位后剩下的数字与反转数字的大小,例如构成的12就比移两位剩下的值1233小,就可以证明还没有达到原位数的一半。这种方法的缺陷是整10的数不能适应这个规则,得单独讨论。
class Solution {
public:
bool isPalindrome(int x) {
if(x < 0) return false;
if(x < 10) return true;
if(x % 10 == 0) return false;
int left = x;
int right = 0;
while(right < left){
right = 10 * right + left % 10;
left /= 10;
}
//位数为奇数和偶数的情况
return left == right || left == right / 10;
}
};