该部分截图来自黑马程序员C++课程戳这里,因为感觉重新打字写的也是一样的东西,不如截图方便
如果有设定值,按照设定值填充。
deque的结构
###########################
题目截图来自LeetCode官网,编程刷题跟着代码随想录学的,主要记录自己刷题过程中的学习结果和查漏补缺的笔记。
题目难度:简单
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/binary-search
关键点:边界值设定和查找法
//Cpp写法
class Solution {
public:
int search(vector<int>& nums, int target) {//使用vector大概是可以保证默认升序排序
int left=0;
int right=nums.size()-1;
while(left<=right){
int middle=(left + right )/ 2;//除法直接保留整数部分
//代码随想录中给出的是left+((right-left)/2)防止溢出,等价于我写的语句,我没有反应过来啥意思
if(nums[middle]<target){
left=middle+1;
}
else if(nums[middle]>target){
right=middle-1;
}
else{
return middle;
}
}
//遍历所有元素都未找出结果
return -1;
}
};
#python写法
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = (right + left) // 2
#python中/表示浮点数 //表示结果为整数
num = nums[mid]
if num == target:
return mid
elif num > target:
right = mid - 1
else:
left = mid + 1
return -1
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
OS:显然暴力遍历的算法是不能满足要求的,虽然不知道系统如何辨识出来
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/search-insert-position
//cpp运行速度快,代码量高,考察严密的逻辑
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left=0;
int right=nums.size()-1;
while(left<=right){
int middle=(left+right)/2;
if(nums[middle]<target){
left=middle+1;
}
else if(nums[middle]>target){
right=middle-1;
}
else{
return middle;
}
}
return left;
}
};
#python写起来就是好看
class Solution(object):
def searchInsert(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
if target in nums:
return nums.index(target)
else:
nums.append(target)
nums.sort()
return nums.index(target)
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array
思路:
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
//write code............
int leftBorder = getLeftBorder(nums, target);
int rightBorder = getRightBorder(nums, target);
// 情况一
if (leftBorder == -2 || rightBorder == -2) return {-1, -1};
// 情况三
if (rightBorder - leftBorder > 1) return {leftBorder + 1, rightBorder - 1};
// 情况二
return {-1, -1};
}
private:
int getRightBorder(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] > target) {
right = middle - 1;
} else { // 寻找右边界,nums[middle] == target的时候更新left
left = middle + 1;
rightBorder = left;
}
}
return rightBorder;
}
int getLeftBorder(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
while (left <= right) {
int middle = left + ((right - left) / 2);
if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
right = middle - 1;
leftBorder = right;
} else {
left = middle + 1;
}
}
return leftBorder;
}
};
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/sqrtx
考点:变量类型存储空间也是代码逻辑完备性考察的因素,编程系统中会给出所有可能的案例,用案例通过率检验你的方法
class Solution {
public:
int mySqrt(int x) {
int i;
for(i=0;i<x;i++){
if((i*i<=x))
{
if(i==46340)
break;
else if((i!=46340)&&((i+1)*(i+1)>x))
break;
}
}
return i;
}
};
双指针法
题目有时间复杂度和空间复杂度的要求
活用容器 时间复杂度不一定满足要求
题目:给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
PS:这道题对非递减含义的理解很重要,下面给个例子
1,2,3,4,5:递增排列,
9,8,7,6,5:递减排列。
1,2,3,3,4,5,8,8:非递减排列,
9,8,7,7,6,5,5,2,1 : 非递增排列。
即,非递减序列中允许有重复值数据
链表与数组的区别:
题目: 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
方法一:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
//删除头结点
while(head!=NULL&& head->val==val){
//用if不能通过所有的测试用例
//假设链表中所有的元素都是val,那么删除第一个head之后
//更新的head仍然需要看作头结点删除
//第二个板块只能解决从head->next开始的val值
//编程题会给出所有可能的测试用例,测试通过的覆盖率不达100%是无法提交通过的
//因此想法一定要全面
ListNode *temp=head;
head=head->next;
delete temp;
}
//删除链表中的非头结点val
ListNode *cur=head;
while(cur!=NULL&&cur->next!=NULL){
//因为遍历链表中的非头结点元素,如果cur代表最后一个元素,那么需要借助前一个元素完成指针指向的改变
//因此,判断是否为val,其实目标对象是cur->next
//类似于for(int i=0;i
if(cur->next->val==val){
ListNode *temp=cur->next;
cur->next=cur->next->next;
delete temp;
}
else
{
cur=cur->next;//完成遍历动作
}
}
return head;//经常忘记写return啦
}
};
方法二:构造虚拟头结点
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作
ListNode* cur = dummyHead;
while (cur->next != NULL) {
if(cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
} else {
cur = cur->next;
}
}
head = dummyHead->next;
delete dummyHead;//最后记住要把虚拟节点删掉
return head;
}
};
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
};
// 初始化链表
MyLinkedList() {
dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
size = 0;
}
int get(int index) {//获取链表下index对应的数值
if (index > (size - 1)|| index < 0) {
return -1;
}
LinkedNode* cur=dummyHead->next;
while(index--){
cur=cur->next;
}
return cur->val;
}
void addAtHead(int val) {//插入成为头节点
LinkedNode* head=new LinkedNode(val);
head->next=dummyHead->next;
dummyHead->next=head;//这个虚拟头节点仍然存在
size++;
}
void addAtTail(int val) {//插入成为尾节点
LinkedNode* tail=new LinkedNode(val);
LinkedNode* cur = dummyHead;
while(cur->next!=nullptr){
cur = cur->next;
}
cur->next = tail;
size++;//一直保持对size的计数是因为整个类内的操作可能是相互关联的
}
void addAtIndex(int index, int val) {//插入指定位置,不包括尾节点
if(index>size){
return;
}
LinkedNode* addNode = new LinkedNode(val);
LinkedNode* cur = dummyHead;
while(index--){
cur=cur->next;
}
addNode->next=cur->next;
cur->next=addNode;
size++;
}
void deleteAtIndex(int index) {//删除指定位置的节点
if(index<0||index>=size){
return;
}
LinkedNode* cur=dummyHead;//在上一道题目中解释了虚节点的好处
while(index--){
cur=cur->next;
}
LinkedNode* tmp=cur->next;
cur->next=cur->next->next;
delete tmp;
size--;
}
// void printNodeList(){
// LinkedNode* cur = dummyHead;
// while(cur->next!=nullptr){
// cout<next->val<<" ";
// cur=cur->next;
// }
// cout<
// }
private:
int size;
LinkedNode* dummyHead;
};
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList* obj = new MyLinkedList();
* int param_1 = obj->get(index);
* obj->addAtHead(val);
* obj->addAtTail(val);
* obj->addAtIndex(index,val);
* obj->deleteAtIndex(index);
*/
知识点补充:null可以用来代表数值为0,nullptr专指空指针
null和nullptr的区别
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };在实际的笔试题目中可能这种的结构体定义,以及前面的库函数的调用都要自己写,否则会执行报错
*/
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* virnode=new ListNode(0);
virnode->next = head;
ListNode* cur = virnode;
while(cur->next!=nullptr && cur->next->next!=nullptr)
//确保不是最后一个结点,它包括值不为0,后继存在
{
ListNode* temp=cur->next;//两两一组中的第一个节点
ListNode* temp1=cur->next->next->next;//下一个两两一组的第一个节点
cur->next=cur->next->next;//让虚拟节点指向第二个节点
cur->next->next=temp;//让第二个节点指向第一个节点
cur->next->next->next=temp1;//让第一个节点指向下一组的第一个节点
//到此一组交换完毕,虚拟头节点需要进入下一组互换,即往前走两步,
//此时不用管指向的到底是哪个节点,反正跨国当前这一组的两个节点即可
cur = cur->next->next;
}
return virnode->next;
//其实这个链表已经实现了两两互换,但是虚拟头节点不能真实参与,
//所以返回的头节点应当是实际链表中的第一个节点,因此返回virnode->next
}
};
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
int count=0;
ListNode* removeNthFromEnd(ListNode* head, int n) {
//两种结题看法:1. 真正改变链表的结构 2. 保证输出符合题目的要求
//递归法解决问题
if(!head) return NULL;//nullptr关键字是小写,null关键字是大写
head->next = removeNthFromEnd(head->next, n); //明明是倒数第n个为什么感觉都是正向走了n步呢?
count++;
if(n==count) return head->next;
return head;
}
};
//这里其实有一个前景规律没有人解释,对于一个序列长L,要找到倒数第n个数并删除
//在有虚拟节点的情况下
//快慢指针方法中,慢指针需要走到倒数第n个数的前一个数的位置处,即该数在L-n+1处,
//慢指针需要走L-n步
//慢指针行走时,快指针与其同步,因此快指针走到null的剩下步伐为L+1-(L-n)=n+1
//因此,为了后续操作,快指针需要先走出(n+1)步
代码随想录和我想的方案一样,但是在帖子里看见一个更巧妙好玩的方法,我将分别写在方案一和方案二中。方法二太好玩了,我把帖子提供给大家。戳这里
方法一,灵感来源于双指针法删除倒数第N个节点。两个链表如果有长度差,必定在开头,先让长的链表将差异走完,剩下的两人同步进行,同步比较即可获得结果。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0, lenB = 0;
while (curA != NULL) { // 求链表A的长度
lenA++;
curA = curA->next;
}
while (curB != NULL) { // 求链表B的长度
lenB++;
curB = curB->next;
}
curA = headA;
curB = headB;
// 让curA为最长链表的头,lenA为其长度
if (lenB > lenA) {
swap (lenA, lenB);
swap (curA, curB);
}
// 求长度差
int gap = lenA - lenB;
// 让curA和curB在同一起点上(末尾位置对齐)
while (gap--) {
curA = curA->next;
}
// 遍历curA 和 curB,遇到相同则直接返回
while (curA != NULL) {
if (curA == curB) {
return curA;
}
curA = curA->next;
curB = curB->next;
}
return NULL;
}
};
方法二,偏重于一种运动策略设计,抓住两路的共性,构建策略方程,完成比较
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* A=headA;
ListNode* B=headB;
while (A != B) {//感觉换做我自己,会想到把两个指针进行比较是不太可能的
A = A != nullptr ? A->next : headB;
B = B != nullptr ? B->next : headA;
}
return A;
}
};
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
if (slow == fast) {
ListNode* index1 = fast;
ListNode* index2 = head;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index2; // 返回环的入口
}
}
return NULL;
}
};
哈希表的基本定义:经过映射方法的处理,将数据以索引和对应的键值成对存储,数据查找时找出key就可以得到相应的value。如果发生键值冲突,即不满足一一对应的关系,哈希表将重新构建。
哈希碰撞解决办法:拉链法和线性探测法。
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/valid-anagram
著作权归领扣网络所有。
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
for (int i = 0; i < s.size(); i++) {
// 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
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) {
// record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
return false;
}
}
// record数组所有元素都为零0,说明字符串s和t是字母异位词
return true;
}
//这种方法没有直接使用哈希容器,但是借助了容器的思想,并且巧用++和--,减少了变量的定义
};
给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
这道题需要去重找元素,数组是无法做到的,于是用哈希结构来解决,势必要用到STL容器,这也是本题的收获要点。
关于STL-set的具体内容介绍戳这里
常见用法:if(nums_set.find(num)!=nums_set.end())
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set;
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for(int num : nums2)
//for(数据类型 A:B):叫做for each循环,是在B中遍历A的意思
{
// set.find()判断在set容器中是否存在某值为x的元素
//set.find()返回一个指向被查找到元素的迭代器;如果没有找到,返回指向集合最后一个元素的迭代器。
//所以当返回值与set.end()不相等时候说明遇见了已经存储过的数据,对于本题而言,也就是找到了nums1和nums2交集中的元素,需要记录在result_set容器中
if(nums_set.find(num)!=nums_set.end()){
//set.end()为set容器中最后一个元素后面的迭代器
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
class Solution {
public:
int getSum(int n){
int sum=0;//定义的变量要赋初值,否则编译错误,我想是因为此函数内外定义了同名变量
while(n){//分解各位数求和
sum +=(n%10)*(n%10);
n/=10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> set;
while(1)
{
int sum=getSum(n);//对当前数的各位按要求求和,尚未形成循环
if(sum==1)
{
return true;//跳出循环
}
if(set.find(sum)!=set.end())
{
return false;//跳出循环
}
else
{
set.insert(sum);
}
n=sum;//循环形成了
}
}
};
因为要返回元素的下标,所以用带有key-value结构的map是最合适的。我们当然可以用暴力循环的方式来解决该问题,但是这样的算法时间复杂度很高。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int>hashtable; // 建立哈希表
for(int i=0;i<nums.size();++i){ //nums.size后面要带括号()
// for(auto i:nums) 错误,因为只有知道i的类型才可以用auto
auto it=hashtable.find(target-nums[i]); //返回类型是iterator迭代器
if(it!=hashtable.end()){ // 查找it是否在hashtable里
return{it->second,i}; //first是键(key),second是值(value)
//hashtable[nums[i]]=i,first就是nums[i],second就是i
}
hashtable[nums[i]]=i; //存入键值对。 hashtable(nums[i])=i;错误,是[]不是()
}
return{};
}
};
class Solution {
public:
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
unordered_map<int, int> umap;
//key-value结构中各自存放数值的含义我们是可以自己定的
for(int a : A){
for(int b:B){
umap[a+b]++;
}
}
int count =0;
for(int c: C){
for(int d:D){
if(umap.find(0-(c+d))!=umap.end()){
count+=umap[0-(c+d)];
}
}
}
return count;//这道题意思明白但是逻辑没搞懂,明明nums4已经确定使用下表为1对应的数值了,为什么还要遍历D
}
};
方案一:
//代码随想录说对于海量数据使用数组会比使用map更快,不过数组实现的方式相当于在构造哈希
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26] = {0};
//add
if (ransomNote.size() > magazine.size()) {
return false;
}
for (int i = 0; i < magazine.length(); i++) {
// 通过recode数据记录 magazine里各个字符出现次数
record[magazine[i]-'a'] ++;
}
for (int j = 0; j < ransomNote.length(); j++) {
// 遍历ransomNote,在record里对应的字符个数做--操作
record[ransomNote[j]-'a']--;
// 如果小于零说明ransomNote里出现的字符,magazine没有
if(record[ransomNote[j]-'a'] < 0) {
return false;
}
}
return true;
}
};
方案二:用暴力算法
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
for(int i=0;i<ransomNote.length();i++){
for(int j=0;j<magazine.length();j++)
{
if(ransomNote[i]==magazine[j]){
ransomNote.erase(ransomNote[i]);//这个函数在学习基础知识的时候并不知道
//感觉在课本中也不常见,需要积累
break;
}
}
}
if(ransomNote.length()==0){
return true;
}
return false;
}
};
//我按照上述方式写的时候编译没有问题,但是测试用例没有百分百通过
//目前还不明白为什么,在我看来也不过就是和下面代码随想录给的示例遍历的先后顺序不一样
//我有想到过,当先遍历ransomNode的时候,在后续删除其重复元素后回到循环语句,
//此时起点相对于原来的定义发生了变化,但好像还是不能证明什么。
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
for (int i = 0; i < magazine.length(); i++) {
for (int j = 0; j < ransomNote.length(); j++) {
// 在ransomNote中找到和magazine相同的字符
if (magazine[i] == ransomNote[j]) {
ransomNote.erase(ransomNote.begin() + j); // ransomNote删除这个字符
break;
}
}
}
// 如果ransomNote为空,则说明magazine的字符可以组成ransomNote
if (ransomNote.length() == 0) {
return true;
}
return false;
}
};
相比较这个逻辑我觉得还是方案一数组给的更加简单直接
有个基础知识必须先解决:C++各容器之间的区别
vector使用方法详解
使用双指针的概念解使用双指针的概念解题。需要注意的是,对于这里的“指针”这个概念,它并不是指编程语言中的指针,而是指两个引导针,便于理清逻辑,当然也不排除真的有需要指针才能实现的题目。双指针法要根据具体的任务和概念选择具象化的方式。
这个办法我想到了,我的设计是:首先对给定的整数数组进行去重和排序,得到一个整理后的数组A。对于A,用for循环给定一个起点数字,剩下的两个数用指针指向他们,模仿冒泡排序的遍历方式,两个指针接着给定的起点,在后面间隔0-n个进行位置移动,找到一个符合条件的元组就记录下来,这样获得的元组一定不会有重读的内容,能够符合条件。但是指针移动的描述条件每次都是判断是否==0,似乎还不够简洁,除此以外,在给定的起点之后,可能的结果要在剩下的元素中两两配对都遍历一遍,我觉得这个思路是行得通的,但是这种思路的缺点就在于,你不是去“找”解析解而是去判断所有可能的配对情况,这样仍然会产生加高的时间复杂度。
代码随想录中给出的思路也是使用双指针,有种逐步代入,找到最优解的感觉。当我确定一个起始点的数值,我要做的是抓住数组已经排序的特性,给指针剩余数据中的最大值和最小值,搭配大了,最大值就要减小,搭配小了,最小值就要增大,这样在基于已经排序的基础之上每一个给定起始点的遍历是唯一的,不需要去测试所有可能的搭配案例。接下来,就需要解决防止元组内容重复的问题。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
//根据函数名定义中提供的返回值类型可以给出如下设定
vector<vector<int>> result;
sort(nums.begin(),nums.end());//这里的sort函数并不能去重
for(int i=0;i<nums.size();i++){
if(nums[i]>0){//经过排序的数组如果起始点大于0,那后面就不用看了
return result;
}
if(i>0 && nums[i]==nums[i-1]){//这里其实是在给数组去重,重复的元素为起点会产生重复的元组
//这里为什么是nums[i]==nums[i-1]而不是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{
//到这里还不能去除所有重复的元组
//举个例子:[2,-1,-1,-1,-1],此时(left,right)的坐标不同有两种情况,但是元组元素是相同的,题目判定为重复元组
//所以,接下来要对剩下两个位置的数去重
while(right>left && nums[right]==nums[right-1]) right--;
while(right>left && nums[left]==nums[left+1]) left++;
//这里必须要使用while循环,如果使用if,只能去除一次重复
result.push_back(vector<int>{nums[i],nums[left],nums[right]});
//找到答案并去重以后,双指针需要同时收缩
//下面这两句话是为了实现while循环里找到一个答案后
//可以在第一个元素起点相同的情况下找到其它符合条件的方案
//也只有找到了一个答案后才会在这个基础之上继续寻找
right--;
left++;
}
}
}
return result;
//不得不说这道题考虑还是要非常周全非常细致才行
}
};
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
// vector>二维动态数组,用于存储所有的元组答案
vector<vector<int>> result;
sort(nums.begin(),nums.end());
//在做完三数之和之后,这道题的难点在于target不再是0,而可能是任意一种情况
//要知道不断改变起始点的时候,即使起始点大于target,不能就直接跳过,因为负数只会越加越小
for(int k=0;k<nums.size();k++){
if(nums[k]>target && nums[k]>=0){//这其实是分情况讨论之起点以后所有的数字都是自然数
break;//跳出循环
}
if(k>0 && nums[k]==nums[k-1]){
continue;//跳过某次迭代
}
//先把全是正数的情况完成?
for(int i=k+1;i<nums.size();i++){//二级排除
//需要排除(N数-指针数)级
if(nums[k]+nums[i]>target && nums[k]+nums[i]>=0){
break;
}
if(i>k+1 && nums[i]==nums[i-1]){
continue;
}
int left=i+1;
int right = nums.size()-1;
while(right>left){
if((long)nums[k]+nums[i]+nums[left]+nums[right]==target){
//不添加这个long会报错,无法通过测试用例
// result.push_back(vector{nums[k],nums[i],nums[left],nums[right]});
result.insert(result.end(), {nums[k], nums[i], nums[left], nums[right]});
while(right > left && nums[left]==nums[left+1]) left++;
//right > left && 随想录中给的,我觉得重复了,因为while循环本身的条件就是这个,但是不加这个确实会报错,还没参透内因
while(right > left && nums[right]==nums[right-1]) right--;
right--;
left++;
}
else if((long)nums[k]+nums[i]+nums[left]+nums[right]>target){
right--;
}
else if((long)nums[k]+nums[i]+nums[left]+nums[right]<target){
left++;
}
}
}
}
return result;
}
};
PS:经验总结:刷题要考虑数据在工业级场景下的注意事项
特别提示:
在(long)nums[k]+nums[i]+nums[left]+nums[right]==target这个语句中,将nums[k]、nums[i]、nums[left]、nums[right]强制转换为long类型的作用是防止它们相加时溢出。
如果nums[k]、nums[i]、nums[left]、nums[right]的值过大,它们相加后的结果可能会超过int类型所能表示的最大值,从而溢出。为了避免这种情况,可以将这些值先转换为long类型,再进行相加。因为long类型的取值范围比int类型更大,所以即使它们相加后的结果很大,也不会溢出。
需要注意的是,将nums[k]、nums[i]、nums[left]、nums[right]转换为long类型后,它们相加后的结果也需要是long类型。因此,整个表达式的类型为bool,表示判断相加后的结果是否等于target。
补充:
基础知识补充:大多数容器都可以使用size()函数来计算元素的个数,因为size()函数是容器的一个成员函数,用于返回容器中元素的数量。例如,对于vector、list、set、map等容器,都可以使用size()函数来计算容器中元素的数量。另外,对于数组,也可以使用sizeof()运算符来计算元素的数量,例如:
int arr[5] = {1, 2, 3, 4, 5};
int n = sizeof(arr) / sizeof(arr[0]); // 计算数组的元素数量
不能使用strlen()函数来计算容器中的元素数量,是因为strlen()函数是用于计算以’\0’结尾的C风格字符串的长度的,它的计算方式是从字符串的起始位置开始,逐个遍历字符,直到遇到’\0’为止。但是,C++容器中存储的元素不一定是以’\0’结尾的字符串,因此不能使用strlen()函数来计算元素数量。
方案一:
class Solution {
public:
void reverseString(vector<char>& s) {
int L=s.size();
char temp;
for(int i=0;i<(L/2);i++){
temp= s[L-i-1];
s[L-i-1]=s[i];
s[i]=temp;
}
}
};
方案二:
class Solution {
public:
void reverseString(vector<char>& s) {
for (int i = 0, j = s.size() - 1; i < s.size()/2; i++, j--) {
swap(s[i],s[j]);
//面试手撕代码最好不要用现成的函数,除非你能接受面试官对底层实现的拷打
}
}
};
PS:在python中一行代码搞定:s[:] = reversed(s)
常规思路:模拟反转字符串的动作。其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。因为要找的也就是每2 * k 区间的起点,这样写,程序会高效很多。
class Solution {
public:
string reverseStr(string s, int k) {//感觉题目默认输入的字符串不是以'\0'结尾
for(int i=0; i<s.size();i+=(2*k))
//与其在后面的程序中按照step更改数组下标,不如在这里直接改变i的step大小
//不管是对于剩余字符还是对前面每一组字符,都是对以i为起点的k以内长度的字符做处理
//简单题重在理清楚框架顺序,中等题会存在很多内部的逻辑完备性考验
{
if(i+k<=s.size()){
//用于处理长度为K的字符串,包括剩余字符(>k && <2k)的情况
reverse(s.begin()+i,s.begin()+i+k);
}
else{//用于处理剩余字符(
reverse(s.begin()+i,s.end());
}
}
return s;
//如果有严格要求,我们也可以自己设计反转函数,输入为字符串、起始点位置、终点位置,输出为反转后的结果
举例:
void reverse(string& s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
swap(s[i], s[j]);
}
};
class Solution {
public:
string replaceSpace(string s) {
//首先需要注意到的问题是,当逐个遍历替换内容的时候,当前位置之后所有的元素都需要移动
//这种操作是什么不简洁的
//改进的方法减少移动的频次(不是想着尽量不搬移)
int count=0;
int L1=s.size();
for(int i=0;i<L1;i++)
{
if(s[i]==' '){
count++;
}
}
s.resize(s.size()+count*2);//resize()函数记一下
int L2=s.size();
//可以开始直接从后往前加元素
//从原字符串最后一个字符开始向前检索赋值给后面的元素
for(int i=L2-1, j=L1-1;i>j ;j--,i--){//判断条件我还没反应过来,但的确需要这个
if(s[j]!=' '){
s[i]=s[j];
}
else{
s[i]='0';
s[--i]='2';
s[--i]='%';
//在已有的代码随想录的指导下做了点小修改,简化代码量
}
}
return s;
}
};
使用python来解决这道题,python自身的功能会给予更多的灵活性
方案二:
class Solution:
def replaceSpace(self, s: str) -> str:
res = []
for i in range(len(s)):
if s[i] == ' ':
res.append('%20')
else:
res.append(s[i])
return ''.join(res)
熟练使用python的特殊功能会精简很多代码量
题外话补充学习:
A. C++中的erase是一个成员函数,用于从容器中删除元素。
它有几种不同的用法,具体如下:
container.erase(iterator);
其中,container是容器对象,iterator是指向要删除元素的迭代器。此时,erase函数将删除iterator指向的元素,并返回指向被删除元素下一个元素的迭代器。如果要删除容器中的最后一个元素,可以使用pop_back函数。
container.erase(start, end);
其中,container是容器对象,start和end是迭代器,指定了要删除的元素范围。此时,erase函数将删除从start到end之间的所有元素,并返回指向被删除元素下一个元素的迭代器。
container.erase(remove_if(container.begin(), container.end(), condition), container.end());
其中,container是容器对象,condition是一个函数对象或lambda表达式,用于判断元素是否应该被删除。此时,erase函数将删除满足condition条件的所有元素,并返回指向被删除元素下一个元素的迭代器。
注意,需要在删除元素后更新迭代器的位置,否则可能会导致未定义的行为。此外,如果容器中的元素是指针或其它引用类型,需要注意在删除元素后,对应的指针或引用可能会失效。
B. 容器与迭代器之间的关系(侯捷老师的STL专题课程里有详细说明)
在C++中,容器是一种可以存储和管理一组数据的数据结构。容器中的数据可以是各种类型,例如整型、浮点型、字符串、对象等。容器可以分为序列式容器和关联式容器两种类型。常见的序列式容器有vector、list、deque等,而常见的关联式容器有map、set等。
迭代器(Iterator)是一种用于访问容器中元素的对象,类似于指针的概念。通过迭代器可以访问容器中的元素,包括读取元素的值、修改元素的值、插入元素和删除元素等操作。迭代器是一个抽象的概念,不同类型的容器对应不同类型的迭代器。
迭代器通常定义在容器中,可以通过容器的成员函数begin()和end()获取容器的迭代器。其中,begin()返回一个指向容器中第一个元素的迭代器,end()返回一个指向容器中最后一个元素的下一个位置的迭代器。使用迭代器可以遍历容器中的元素,例如:
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << std::endl;
}
上述代码中,vec.begin()返回一个指向vec中第一个元素的迭代器,vec.end()返回一个指向vec中最后一个元素的下一个位置的迭代器。在循环中,通过迭代器遍历容器中的元素,并输出它们的值。
需要注意的是,对一个迭代器进行解引用(*it)操作,可以获取到该迭代器指向的元素的值。此外,迭代器也可以进行算术运算,例如it++和it–,以及it+n和it-n等操作,其中n为整数。所以它和指针的作用效果及功能看起来真的挺像的。
其实如果学透了python的诸多功能,解决算法题可能是最节省时间的。
这道题用python做效果真的炸裂。
class Solution:
def reverseWords(self, s: str) -> str:
return " ".join(s.split( )[::-1])
熟悉掌握python中的各种函数和功能并灵活使用是真的香!但是面试官还是会倾向于不使用标准库工具的人才
class Solution {
public:
void reverse(string& s, int start, int end){ //翻转,区间写法:左闭又闭 []
for (int i = start, j = end; i < j; i++, j--) {
swap(s[i], s[j]);
}
}
void removeExtraSpaces(string& s) {//去除所有空格并在相邻单词之间添加空格, 快慢指针。
int slow = 0; //整体思想参考https://programmercarl.com/0027.移除元素.html
for (int i = 0; i < s.size(); ++i) { //
if (s[i] != ' ') { //遇到非空格就处理,即删除所有空格。
if (slow != 0) s[slow++] = ' '; //手动控制空格,给单词之间添加空格。slow != 0说明不是第一个单词,需要在单词前添加空格。
while (i < s.size() && s[i] != ' ') { //补上该单词,遇到空格说明单词结束。
s[slow++] = s[i++];
}
}
}
s.resize(slow); //slow的大小即为去除多余空格后的大小。
}
string reverseWords(string s) {
removeExtraSpaces(s); //去除多余空格,保证单词之间之只有一个空格,且字符串首尾没空格。
reverse(s, 0, s.size() - 1);
int start = 0; //removeExtraSpaces后保证第一个单词的开始下标一定是0。
for (int i = 0; i <= s.size(); ++i) {
if (i == s.size() || s[i] == ' ') { //到达空格或者串尾,说明一个单词结束。进行翻转。
reverse(s, start, i - 1); //翻转,注意是左闭右闭 []的翻转。
start = i + 1; //更新下一个单词的开始下标start
}
}
return s;
}
};
太累了,还没有搞得很懂,有时间再上解析吧
有点暴力的算法
class Solution {
public:
string reverseLeftWords(string s, int n) {
int count=0;
while(count<n){
char temp=s[0];
for(int j=1;j<s.size();j++){
s[j-1]=s[j];
//每次将数组首位放置在尾部的过程都需要经过位置的翻转就其实还挺烦的
}
s[s.size()-1]=temp;
count++;
// cout<
}
return s;
}
};
不使用额外空间的算法
class Solution{
public:
string reverseLeftWords(string s, int n){
reverse(s.begin(), s.begin()+n);
reverse(s.begin()+n, s.end());
reverse(s.begin(), s.end());
//经过这三部反转过程就能实现题目要求,就蛮好玩的
//感觉灵感还是经验得来的
// cout<
return s;
}
};//class末尾的分号不要忘记加嗷
先来一个朴素模式匹配算法(截图来自B战王道数据结构与算法课,建议听)
C++中find函数的用法
对于普通数组,find(开始位置,结束位置+1,要查找元素)返回所查找元素的地址,如果需要知道元素的下标,还需减去数组首地址。
对于字符串,find函数返回值:若找到,返回第一次出现的位置的下标;否则返回 -1。
这正是题目所要求的。
public static int bf(String ts, String ps) {//在ts字符串中找ps匹配段
char[] t = ts.toCharArray();
char[] p = ps.toCharArray();
int i = 0; // 主串的位置
int j = 0; // 模式串的位置
while (i < t.length && j < p.length) {
if (t[i] == p[j]) { // 当两个字符相同,就比较下一个
i++;
j++;
} else {
i = i - j + 1; // 一旦不匹配,i后退
//如果i当前位置开始的长度为(j+1)的字段不能完成整体匹配,那么要从首个匹配字符的下一个位置开始重新找连续的字符串
j = 0; // j归0
}
}
if (j == p.length) {
return i - j;
} else {
return -1;
}
}
如果不讲究其他因素的考虑,对C++拥有的功能函数掌握的比较熟悉,可以使用一行代码解决问题
class Solution {
public:
int strStr(string haystack, string needle) {
return haystack.find(needle);
}
};
这题得讲讲KMP算法啦~我感觉面试官会问的,属于数据结构与算法的其中一项吧
从面试问答的角度好好学习一下吧
命名:发明人的名字
功能:主要就是为了解决字符串匹配问题
时间复杂度:对于一个长度为n的字符串,以及长度为m的匹配字符串,其时间复杂度为O(m+n)。
KMP算法中主要需要理解的就是 最长公共前后缀以及next数组的思想
功能:当匹配失败时能够记录前面已经匹配的部分内容,并返回下次该从哪个位置开始。
个人理解的备注:
void getNext(int* next, const string& s){
int j = -1;
next[0] = j;
for(int i = 1; i < s.size(); i++) { // 注意i从1开始
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
//代码随想录的B站视频中有提到,小心将这个while错写为if,因为不断加长度的过程是要依靠循环实现的,只有找到了匹配长度才会跳出,并记录下来
j = next[j]; // 向前回退
}
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
class Solution {
public:
void getNext(int* next, const string& s) {
int j = -1;
next[0] = j;
for(int i = 1; i < s.size(); i++) { // 注意i从1开始
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = -1; // // 因为next数组里记录的起始位置为-1
for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始
while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配
j = next[j]; // j 寻找之前匹配的位置
}
if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串t
return (i - needle.size() + 1);
}
}
return -1;
}
};
分享一个妙蛙种子
思路介绍:(学习自leetcode上的网友分享)
class Solution:
def repeatedSubstringPattern(self, s: str) -> bool:
return s in (s+s)[1:-1]
# 如果你的代码有语法错误,python中的颜色会变成红色
相同的思想写成C++
浅浅翻译一下Python中的意思
class Solution {
public:
bool repeatedSubstringPattern(string s) {
//就是说对于基础语言的各种写法不要认为不重要,灵活使用后就知道好处了
string newstr=s+s;
newstr = newstr.substr(1,newstr.size()-2);//提供起始位置和截取长度
//返回一个string,包含s中从pos开始的n个字符的拷贝(pos的默认值是0,n的默认值是s.size() - pos,即不加参数会默认拷贝整个s)
if(newstr.find(s)==-1)
return false;
return true;
}
};
代码随想录提供
class Solution {
public:
bool repeatedSubstringPattern(string s) {
string t = s + s;
t.erase(t.begin()); t.erase(t.end() - 1); // 掐头去尾
//学习erase函数的语法格式,它可以删除一段也可以删除一个字符,
//但是在本文中删除的内容不是连续的,因此只能首尾定位删除。
if (t.find(s) != std::string::npos) return true; // r
//这其实是一种比较老的写法了,一般代码开头我们已经使用 using namespace std 并且引入了include,基本上就不需要这么麻烦的调用了。
return false;
}
};
用栈来实现队列的功能,那么栈有自己的top() pop()等功能函数吗?
class MyQueue {
public:
stack<int> stIn;
stack<int> stOut;
/** Initialize your data structure here. */
MyQueue() {
}
/** Push element x to the back of queue. */
void push(int x) {
stIn.push(x);
}
/** Removes the element from in front of queue and returns that element. */
int pop() {
// 只有当stOut为空的时候,再从stIn里导入数据(导入stIn全部数据)
if (stOut.empty()) {
// 从stIn导入数据直到stIn为空
while(!stIn.empty()) {
stOut.push(stIn.top());
stIn.pop();
}
}
int result = stOut.top();
stOut.pop();
return result;
}
/** Get the front element. */
int peek() {
int res = this->pop(); // 直接使用已有的pop函数
stOut.push(res); // 因为pop函数弹出了元素res,所以再添加回去
//对于这个弹出元素后再塞回去是个什么意思,我不是很懂,不塞回去就不行吗
//题目不是只要求在该子函数功能中能够实现元素的输出吗
return res;
}
/** Returns whether the queue is empty. */
bool empty() {
return stIn.empty() && stOut.empty();
}
};
push() 在队尾插入一个元素
pop() 删除队列第一个元素
size() 返回队列中元素个数
empty() 如果队列空则返回true
front() 返回队列中的第一个元素
back() 返回队列中最后一个元素
queue<int> que;
que.push(1); //入队列 que中为: 1
que.push(2); //入队列 que中为: 1 2
cout << que.size() << endl; // 输出2
cout << que.front()<<endl; // 输出1
cout << que.back() << endl; //输出2
cout << que.empty() << endl; / /输出false
que.pop(); //出队列 que中为:2
class MyStack {
public:
queue<int> que;
/** Initialize your data structure here. */
MyStack() {
}
/** Push element x onto stack. */
void push(int x) {
que.push(x);
}
/** Removes the element on top of the stack and returns that element. */
int pop() {
int size = que.size();
size--;
while (size--) { // 将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部
que.push(que.front());//在队尾插入一个元素
que.pop();//删除队列第一个元素
}
int result = que.front(); // 此时弹出的元素顺序就是栈的顺序了
que.pop();
return result;
}
/** Get the top element. */
int top() {
return que.back();
}
/** Returns whether the stack is empty. */
bool empty() {
return que.empty();
}
};
class Solution {
public:
bool isValid(string s) {
if(s.size()%2==0){
int count=s.size()/2;
for(int i=0;i<count;i++){
if(s[i]!=s[count-i-1])
return false;
}
return true;
}
return false;
//该方法只能解决"({[]})",没办法解决"()[]{}"类型的符号串
}
};
top()取栈顶元素,不会删除里面的元素,返回栈顶的引用;
pop()是删除栈顶元素,返回void
栈stack的几个常用函数
class Solution {
public:
string removeDuplicates(string S) {
stack<char> st;//注意,这里设置的type是char不是string
for(char s:S){
if(st.empty()||s!=st.top()){//对于这个判断,要理解清楚
//如果是空栈,我们就可以放入元素,又因为只删除相邻重复的元素,
//所以如果即将放入的元素与栈顶不同,也可以放入栈中
st.push(s);
}
else{//如果遇见相同的元素,那么该元素不用放入栈中,同时还需要把栈顶的元素删除
st.pop();
}
}
string result="";
while(!st.empty()){//由于st是栈型,没有办法用while(st)的方式判断是否为空
result +=st.top();
st.pop();
}
reverse(result.begin(),result.end());
return result;
}
};
方案二:扣友提供的原地算法,蛮好玩
class Solution {
public:
string removeDuplicates(string S) {
int top = 0;
for (char ch : S) {
if (top == 0 || S[top - 1] != ch) {
S[top++] = ch;
} else {
top--;
}
}
S.resize(top);
return S;
}
};
熟悉C++中的多种功能函数,总是会很好玩的
逆波兰式计算机专业的人都知道,我就不多说了,操作系统还是计算机组成原理里面的基础知识。
数量掌握栈的概念进行设计,这个问题其实就还行。
对于栈数据较为固定的处理规则要熟悉。
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<long long> st;
for(int i=0;i<tokens.size();i++){
if(tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/"){
//这个时候判断是否是运算符,如果是,那么执行完操作他就被自动跳过了
long long num1=st.top();
st.pop();
long long num2=st.top();
st.pop();
if (tokens[i] == "+") st.push(num2 + num1);
if (tokens[i] == "-") st.push(num2 - num1);
if (tokens[i] == "*") st.push(num2 * num1);
if (tokens[i] == "/") st.push(num2 / num1);
}
else{//当不是操作符那么就现将数值存储进st
//后缀表达式操作的对象正好是运算符的前两个数,符合栈中的运动规律
//同时每一次运算符的结果都会更新覆盖原来的内容,所以不用担心找数据的问题
st.push(stoll(tokens[i]));
//stoll 是 C++ 标准库中的一个函数,位于 头文件中,用于将字符串转换为长长整型 (long long)
}
}
int result=st.top();
// st.pop();//到这里就是说不管st到底是什么状态,因为已经与它无关了
//但考虑到现实的工作状况,可能我们还是希望有一个及时清理内存,保持编程好习惯的思维吧,我是这么想的
return result;
}
};
这道题首先需要统计元素出现的频率,很自然地想到使用map来实现。然后,需要对频率大小进行排序,此时如果使用快排算法,需要将map转化为vector,因为
除此以外,快排并不能在选出前K个数的时候就停止。常规的排序算法只是将所有数据排序后去了前K个输出,这与堆排序中找到了K个就停止是不相同的。
代码随想录中给的是堆排序法,但是我不明白为什么要用堆的方式
遍历无序映射 mp 中的每个键值对 p,将其作为 pair 对象放入优先队列 pq 中。
由于优先队列默认是大根堆,为了让其变成小根堆,将 p 的值取相反数作为优先级。
同时,如果优先队列中元素的个数超过了 k,就弹出堆顶元素。
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
vector<int> ret;
unordered_map<int, int> mp;//一个空的无序映射 mp
priority_queue<pair<int, int>> pq;//一个优先队列 pq
for (auto i : nums) mp[i]++;//统计频次
for (auto p : mp) {
pq.push(pair<int, int>(-p.second, p.first));//容器高级参数的引用
//其中 mp 的键是数组中的元素,值是该元素出现的次数,所以使用priority_queue的时候需要引入second
if (pq.size() > k) pq.pop();
}
while (k--) {
ret.push_back(pq.top().second);
//遍历前 k 个元素,依次将其键值放入结果数组 ret 中,ret为一个vector存放数据
pq.pop();
}
return ret;
}
};
关于优先队列的一点点汇总: