使用二分查找的前提条件:已排序的数组、无重复元素
思想:最关键的是理解区间的定义,是左闭右闭?还是左闭右开?两种区间的定义方式,对应的while循环条件是不一样的,对应的更新后的left,right的取值是不一样的。
题解1:区间定义为[left,right],左闭右闭
int binarySearch1(vector &nums,int target){
int left = 0;
int right = nums.size() - 1;
while(left <= right){//当left==right,区间[left,right]依然有效,所以用<=
int middle = left + ((right - left) / 2);//防止溢出,等同于(left+right)/2
if(nums[middle] > target){
right = middle - 1;//target在左区间,所以[left,middle-1]
}else if(nums[middle] < target){
left = middle + 1;//target在右区间,所以[middle+1,left]
}else{
return middle;
}
}
//未找到目标值
return -1;
}
题解2:区间定义为[left,right),左闭右开
int binarySearch2(vector &nums,int target){
int left = 0;
int right = nums.size();//定义target在左闭右开的区间里,即:[left,right)
while(left < right){//left==right是没有意义的
int middle = left + ((right - left) / 2);
if(nums[middle] > target){
right = middle;//target在左区间[left,middle)
}else if(nums[middle] < target){
left = middle + 1;//target在右区间[middle+1,right)
}else{
return middle;
}
}
//未找到
return -1;
}
解法1:暴力解法
class Solution{
public:
int removeElement(vector &nums,int val){
int size = nums.size();
for(int i = 0;i < size; ++i){
if(nums[i] == val){
/*这种方式不正确,因为nums[j+1]数组会越界*/
/* for(int j = i;j < size;++j){ */
/* nums[j] = nums[j+1]; */
/* } */
for(int j = i+1;j < size;++j){
nums[j-1] = nums[j];
}
i--;//因为下标i以后的元素都向前移动了一位,所以i也向前移动一位
size--;//此时数组的大小减1
}
}
return size;
}
};
解法2:双指针法
双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下,完成两个for循环的工作。
快指针:寻找新数组的元素,新数组的元素就是不含有目标元素的数组。
慢指针:指向更新 新数组下标的位置。
int removeElement2(vector &nums,int val){
int slowIndex = 0;
for(int fastIndex = 0;fastIndex < (int)nums.size(); ++fastIndex){
if(nums[fastIndex] != val){
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
题解1:暴力解法
int searchInsert(vector& nums,int target){
//考虑以下三种情况
//1.插入最开始的位置
//2.若插入元素 和 数组中的某个元素相同
//3.插入数组中,这是最平常的情况
for(int i = 0;i < (int)nums.size();++i){
if(nums[i] >= target){
return i;
}
}
//4.如果target是最大的,或者nums为空,则返回nums的长度
return nums.size();
}
题解2:二分查找
int searchInsert_binary1(vector &nums,int target){
int left = 0;
int right = nums.size() - 1;
while(left <= right){
int middle = left + ((right - left) / 2);
if(nums[middle] > target){
right = middle - 1;
}else if(nums[middle] < target){
left = middle + 1;
}else{
return middle;
}
}
return right + 1;
}
//左闭右开
int searchInsert_binary2(vector &nums,int target){
int left = 0;
int right = nums.size();
while(left < right){
int middle = left + ((right - left) / 2);
if(nums[middle] > target){
right = middle;
}else if(nums[middle] < target){
left = middle + 1;
}else{
return middle;
}
}
return right;
}
解法1:暴力解法。
思想:把数组中的每一个元素都平法,然后再排序
vector sortedSquares(vector &nums){
for(int i = 0;i < nums.size(); ++i){
nums[i] *= nums[i];
}
sort(nums.begin(),nums.end());//快排
return nums;
}
解法2:双指针法
思想:首先定义一个和原数组相同大小的数组,用来存储平方之后的元素。因为原数组中的元素有负数,所以平方之后的最大值只能在数组的两头产生而不可能在中间产生。所以想到双指针的方法。i指向第一个元素,j指向最后一个元素。要注意for循环退出的条件是什么
vector sortedSquares2(vector &nums){
int k = nums.size() - 1;
vector result(nums.size(),0);
for(int i=0,j=nums.size()-1;i <= j;){//注意这里要i <= j,因为最后要处理同一个元素,循环退出条件为i>j
if(nums[i] * nums[i] < nums[j] * nums[j]){
result[k--] = nums[j] * nums[j];
j--;
}else{
result[k--] = nums[i] * nums[i];
i++;
}
}
return result;
}
解法1:暴力解法
//暴力解法:两个for循环,计算每个子串的和
int minSubArrayLen(int target,vector &nums){
int result = INT32_MAX;//最终结果
int sum = 0;//子序列的数值之和
int subLength = 0;//子序列的长度
for(int i = 0;i < (int)nums.size();++i){//设置子序列起点为i
sum = 0;
for(int j = i;j < (int)nums.size();++j){//设置子序列终点为j
sum += nums[j];
if(sum >= target){//一旦发现子序列超过了target,则更新result
subLength = j - i + 1;
result = result < subLength ? result : subLength;
break;//找到符合条件的子序列,但不一定是最短,所以break跳出内层循环,然后让i++
}
}
}
//如果result没有赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
时间复杂度:O(n²)
解法2:滑动窗口
//解法2:滑动窗口,先确定终止位置再移动起始位置。
//用一个for循环,j表示滑动窗口的
int minSubArrayLen2(int target,vector &nums){
int result = INT32_MAX;
int subLength = 0;
int sum = 0;
int i = 0;
for(int j = 0;j < (int)nums.size();++j){
sum += nums[j];
while(sum >= target){
subLength = j - i + 1;
result = result < subLength ? result : subLength;
sum -= nums[i];
i++;
}
}
//如果result没有赋值,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
时间复杂度:O(n);不要认为for循环里面嵌套while循环就是n²,关键看对数据的操作次数,元素在进入滑动窗口操作一次,出滑动窗口操作一次,O(2*n),所以就是O(n)
非常考验代码的掌控能力
vector> generateMatrix(int n){
vector> res(n,vector(n,0));//使用vector定义一个n * n的二维数组
int startX = 0,startY = 0;//定义每循环一圈的起始位置
int loop = n / 2;//每个圈循环几次,例如n为奇数3,只是循环一圈,矩阵中间的值需要单独处理
int mid = n / 2;//矩阵中间的位置,例如n为奇数3,中间的位置就是(1,1)
int count = 1;//用来给矩阵中每一个空格赋值
int offset = 1;//用来控制每一条边遍历的长度,每次循环右边界收缩1
int i,j;
while(loop--){
i = startX;
j = startY;
//下面开始的四个for循环就是模拟转了一圈
//从左到右
for(j = startY; j < n - offset;j++){
res[startX][j] = count++;
}
//从上到下
for(i = startX;i < n - offset;i++){
res[i][j] = count++;
}
//从右到左
for(;j > startY;j--){
res[i][j] = count++;
}
//从下到上
for(;i > startX;i--){
res[i][j] = count++;
}
//第二圈开始的时候,起始位置要各自加1。例如:第一圈起始位置(0,0),第二圈起始位置是(1,1)
startX++;
startY++;
//offset控制每一圈里,每一条边遍历的长度
offset++;
//如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if(n % 2){
res[mid][mid] = count;
}
}
return res;
}
解法1:正常解法,分开处理头结点和中间结点
class Solution{
public:
ListNode* removeElements(ListNode* head, int val) {
// 删除头结点
while (head != NULL && head->val == val) { // 注意这里不是if
ListNode* tmp = head;
head = head->next;
delete tmp;
}
// 删除非头结点
ListNode* cur = head;
while (cur != NULL && cur->next!= NULL) {
if (cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
} else {
cur = cur->next;
}
}
return head;
}
};
注意:Linux下写的怎么会有double free错误,还未解决。但是Leetcode网站上是AC的。
解法2:设置一个虚拟头结点再进行移除节点操作,这样子的话删除真正头结点的操作和非头结点的操作是一样的。
ListNode *removeElements(ListNode *head,int val){
ListNode *dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode *curr = dummyHead;
while(curr->next != nullptr){
if(curr->val == val){
ListNode *tmp = curr->next;
curr->next = curr->next->next;
delete tmp;
}esle{
curr = curr->next;
}
}
head = dummyHead;
delete dummyHead;
return head;
}
class MyLinkedList {
public:
//定义链表节点结构体
struct LinkedNode{
int val;
LinkedNode *next;
LinkedNode(int val) : val(val),next(nullptr){}
LinkedNode(int x,LinkedNode *next) : val(x),next(next){}
};
//初始化列表
MyLinkedList() {
_dummyHead = new LinkedNode(0);//这里定义的头结点是一个虚拟头结点,而不是链表真正的头结点
_size = 0;
}
int get(int index) {
if(index < 0 || index > (_size - 1)){
return -1;
}
LinkedNode *curr = _dummyHead->next;
while(index--){
curr = curr->next;
}
return curr->val;
}
void addAtHead(int val) {
LinkedNode *newNode = new LinkedNode(val);
newNode->next= _dummyHead->next;
_dummyHead->next = newNode;
_size++;
}
void addAtTail(int val) {
LinkedNode *newNode = new LinkedNode(val);
LinkedNode *curr = _dummyHead;
while(curr->next != nullptr){
curr = curr->next;
}
curr->next = newNode;
_size++;
}
void addAtIndex(int index, int val) {
if(index < _size) return ;
if(index > 0) index = 0;
LinkedNode *newNode = new LinkedNode(val);
LinkedNode *curr = _dummyHead;
while(index--){
curr = curr->next;
}
newNode->next = curr->next;
curr->next = newNode;
_size++;
}
void deleteAtIndex(int index) {
if(index >= _size || index < 0){
return ;
}
LinkedNode *curr = _dummyHead;
while(index--){
curr = curr->next;
}
LinkedNode *tmp = curr->next;
curr->next = curr->next->next;
delete tmp;
tmp = nullptr;
_size--;
}
//打印链表
void printLinkedList() const{
LinkedNode *curr = _dummyHead;
while(curr->next != nullptr){
cout << curr->next->val << ' ';
curr = curr->next;
}
cout << endl;
}
private:
int _size;
LinkedNode *_dummyHead;
};
思想:头插法。定义三个指针pre为null,curr指向头结点,temp执行curr->next目的是为了防止短链
ListNode *reverseList(ListNode *head){
ListNode *temp;//保存curr的下一个节点
ListNode *curr = head;//保存当前结点
ListNode *pre = nullptr;
while(curr != nullptr){
temp = curr->next;
curr->next = pre;
//更新pre和curr
pre = curr;
curr = temp;
}
return pre;
}
思想:定义一个虚拟头结点,dummyHead->next=head。一定要画图理清楚指针的指向。
//两两交换相邻的节点
ListNode *swapPairs(ListNode *head){
ListNode *dummyHead = new ListNode(0);
dummyHead->next = head;//将虚拟头结点指向head,这样方便后面的删除操作
ListNode *curr = dummyHead;
while(curr->next != nullptr && curr->next->next != nullptr){
ListNode *tmp1 = curr->next;//记录临时节点
ListNode *tmp2 = curr->next->next->next;//记录临时结点
//交换
curr->next = curr->next->next;
curr->next->next = tmp1;
tmp1->next = tmp2;
//curr移动两位,准备下一轮交换
curr = curr->next->next;
}
return dummyHead->next;
}
思路:双指针法。定义一个虚拟结点,fast和slow初始化为dummyNode。先让fast移动n+1,然后再让slow和fast一起移动,移动到fast==nullptr为止,这样的话slow正好是指向倒数第n个节点的前一个节点。
ListNode *removeNthFromEnd(ListNode *head,int n){
ListNode *dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode *slow = dummyHead;
ListNode *fast = dummyHead;
while(n-- && fast != nullptr){
fast = fast->next;
}
//fast再提前走一步,因为需要让slow指向删除结点的上一个节点
fast = fast->next;
while(fast != nullptr){
fast = fast->next;
slow = slow->next;
}
ListNode *tmp = slow->next;
slow->next = slow->next->next;
delete tmp;
return dummyHead->next;
}
思路:简单来说就是求两个链表交点的指针。特别注意:交点不是数值相等,而是指针相等。为了方便理解,假设节点元素的数值相等,则节点指针相等。
ListNode *getIntersectionNode(ListNode *headA,ListNode *headB){
ListNode *currA = headA;
ListNode *currB = headB;
int lenA = 0,lenB = 0;
while(currA != nullptr){
lenA++;
currA = currA->next;
}
while(currB != nullptr){
lenB++;
currB = currB->next;
}
currA = headA;
currB = headB;
//让currA为最长链表的头,lenA为其长度
if(lenB > lenA){
swap(lenA,lenB);
swap(currA,currB);
}
//求长度差
int gap = lenA - lenB;
//让currA和currB在同一起点上(就是末尾位置对齐)
while(gap--){
currA = currA->next;
}
//遍历currA和currB,遇到相同则直接返回
while(currA != nullptr){
if(currA == currB){
return currA;
}
currA = currA->next;
currB = currB->next;
}
return nullptr;
}
周日休息
思路:前提是假设s和t字符串只包含小写字母。那么定义一个大小为26的整形数组result,用来记录对应小写字母的个数。遍历s将存在的小写字母统计result[s[i] - 'a']++,然后在遍历t再将result--。如果result数组全部为0,那就说明s和t是异位词。
bool isAnagram(string s,string t){
int record[26] = {0};
for(int i = 0;i < s.size();++i){
record[s[i] - 'a']++;
}
for(int i = 0;i < t.size();++i){
record[t[i] - 'a']--;
}
for(int i = 0;i < 26;i++){
if(record[i] != 0){
return false;
}
}
return true;
}
思路:主要是unordered_set的使用,nums1和nums2,首先对nums1存到set中,再定义一个结果set,用来存放两个数组的交集。
vector intersection(vector& nums1, vector& nums2) {
unordered_set result_set; // 存放结果,之所以用set是为了给结果集去重
unordered_set nums_set(nums1.begin(), nums1.end());
for (int num : nums2) {
// 发现nums2的元素 在nums_set里又出现过
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector(result_set.begin(), result_set.end());
}
思路:1.如何求取n各个位。2.理解快乐数的定义,如何理解sum进入无限循环,在代码中如何体现?用一个集合,sum如果曾经出现过,那么就是说明已经进入了无限循环。
//取数值各个位上的单数之和
int getSum(int n){
int sum = 0;
while(n){
sum += (n % 10) * (n % 10);
n = n / 10;
}
return sum;
}
//判断是否是快乐数
bool isHappy(int n){
unordered_set set;
while(1){
int sum = getSum(n);
if(sum == 1){
return true;
}
//如果这个sum曾经出现过,说明已经进入了无限循环,立刻return false
if(set.find(sum) != set.end()){
return false;
}else{
set.insert(sum);
}
n = sum;
}
}
思路:定义一个map,用来存储元素和该元素对应的下边。遍历nums数组,在map中寻找与当前元素匹配的元素,如果没有找到那就将key,value加入到map中。
vector twoSum(vector &nums,int target){
unordered_map map;
for(int i = 0;i < nums.size();++i){
//遍历当前元素,并在map中寻找是否有匹配的key
auto it = map.find(target - nums[i]);
if(it != map.end()){
return {it->second,i};
}
//如果没有找到匹配对,就把访问过的元素和下标加入到map中
map.insert(pair(nums[i],i));
}
return {};
}
思路:
int fourSumCount(vector &A,vector &B,vector &C,vector &D){
unordered_map umap;//key:a+b的数值,value:a+b数值出现的次数
//遍历A和B两个数组,统计两个数组元素之和,和出现的次数,放到map中
for(int &a : A){
for(int &b : B){
umap[a+b]++;
}
}
int count = 0;//统计a+b+c+d=0出现的次数
//遍历C和D数组,找到0-(c+d)在map中出现过的话,就把map中key对应的value也就是出现的次数统计出来
for(int &c : C){
for(int &d :D){
if(umap.find(0-(c+d)) != umap.end()){
count += umap[0 - (c+d)];
}
}
}
return count;
}
思路:哈希解法
//哈希解法:
bool canConstruct2(string ransomNote,string magazine){
int record[26] = {0};
if(ransomNote.size() > magazine.size()){
return false;
}
for(int i = 0;i < magazine.size();++i){
//记录magazine中各个字符出现的次数
record[magazine[i] - 'a']++;
}
for(int j = 0;j < ransomNote.size();++j){
//遍历ransomNote,在record里对应的字符个数做--操作
record[ransomNote[j] - 'a']--;
if(record[ransomNote[j] - 'a'] < 0){
return false;
}
}
return true;
}
思路:双指针法。
vector> threeSum(vector &nums){
vector> result;
sort(nums.begin(),nums.end());
//找出a+b+c = 0
//a = nums[i],b = nums[left],c = nums[right]
for(int i = 0;i < nums.size();++i){
//排序之后如果第一个元素已经大于0,那么无论如何组合都不可能组成三元组,直接返回结果就可以了
if(nums[i] > 0){
return result;
}
//去重a
if(i > 0 && nums[i] == nums[i-1]){
continue;
}
int left = i + 1;
int right = nums.size()-1;
while(right > left){
if(nums[i] + nums[left] + nums[right] > 0) right--;
else if(nums[i] + nums[left] + nums[right] < 0) left++;
else{
result.push_back({nums[i],nums[left],nums[right]});
//去重逻辑应该放在找到一个三元组之后,对b 和c去重
while(right > left && nums[right] == nums[right-1]) right--;
while(right > left && nums[left] == nums[left+1]) left++;
//找到答案时,双指针同时收缩
right--;
left++;
}
}
}
return result;
}
思路:就是相当于字符数组的反转,就是前后交换
void reverseString(vector &s){
for(int i = 0,j = s.size()-1;i < s.size()/2;++i,--j){
swap(s[i],s[j]);
}
}
思路:关键是i的移动,还有是将题目描述的规则写出来
1.将每隔2k个字符的前k个字符进行反转。
2.剩余字符小于2k,但大于等于k个,则反转前k个字符。
3.剩余字符少于k个,则将剩余字符全部反转。
//反转函数
void reverse(string &s,int start,int end){
for(int i = start,j = end;i < j;i++,j--){
swap(s[i],s[j]);
}
}
//实现题目中描述的规则
string reverseStr(string s,int k){
for(int i = 0;i < s.size(); i += (2*k)){
//1.每隔2k个字符的 前k个字符进行反转
//2.剩余字符小于2k 但大于或等于 k 个,则反转前k个字符
if(i + k <= s.size()){
reverse(s,i,i+k-1);
continue;
}
//3.剩余字符少于k个,则将剩余字符全部反转
reverse(s,i,s.size() - 1);
}
return s;
}
思路:先统计空格的个数,然后重新设置string的大小。两个指针一个指针指向字符串的头,另一个指针指向扩展后字符串的尾,然后从后向前遍历,如果遇到非空格那么直接复制,否则的话就进行替换。
string replaceSpace(string &s){
int count = 0;//统计空格的个数
int sOldSize = s.size();
for(int i = 0;i < s.size();++i){
if(s[i] == ' '){
count++;
}
}
//扩充字符串s的大小,也就是每个空格替换成%20之后的大小
s.resize(s.size() + count*2);
int sNewSize = s.size();
//从后往前将空格替换为%20
for(int i = sNewSize-1,j = sOldSize-1;j < i;i--,j--){
if(s[j] != ' '){
s[i] = s[j];
}else{
s[i] = '0';
s[i-1] = '2';
s[i-2] = '%';
i -= 2;
}
}
return s;
}
思路:以空格为分隔符,将字符串读取到字符串输入流中,让后输出到变量word中,并将word存储在vector中,这样就把每个单词存储下来了。然后从后向前遍历vector,进行字符串拼接。
string reverseWords(string &s){
istringstream ss(s);
string word;
vector words;
//以空格为分隔符分割字符串,将单词保存到向量中
while(ss >> word){
words.push_back(word);
}
string result;
//从后往前遍历vector,逐个添加单词到结果字符串中
for(int i = words.size()-1;i >= 0;--i){
result += words[i];
if(i > 0){
result += " ";
}
}
return result;
}
思路:先反转前n个字符,再反转第n个到字符串末尾的字符,然后在从头到尾全部反转。
string reverseLeftWords(string s,int n){
reverse(s.begin(),s.begin()+n);
reverse(s.begin()+n,s.end());
reverse(s.begin(),s.end());
return s;
}
KMP算法:还未掌握,需要看考研的那一块。理解前缀表,为什么要用前缀表,理解next数组。