命名
小驼峰、大驼峰命名法
下划线命名法
匈牙利命名法
水平留白(代码空格)
操作符左右一定有空格
隔符(,
和;
)前一位没有空格,后一位保持空格
大括号和函数保持同一行,并有一个空格
控制语句(while,if,for)后都有一个空格
时间复杂度是一个函数,定性描述该算法的运行时间
O用来表示上界的 快速排序是O(nlogn)的时间复杂度
O(1)常数阶 < O( log n)对数阶 < O(n)线性阶 < O(n log n)线性对数阶 < O(n^2)平方阶 < O(n^3)立方阶 < O(2^n)指数阶
存放在连续内存空间上的相同类型数据的集合
下标都是从0开始的。数组内存空间的地址是连续的。
数组的元素是不能删的,只能覆盖。(C++)
Java是没有指针的,同时也不对程序员暴露其元素的地址,寻址操作完全交给虚拟机。
循环不变量原则
前提:有序数组,无重复元素
1.左闭右闭即[left, right]
while (left <= right) 要使用 <=
if (nums[middle] > target) right 要赋值为 middle - 1
2.左闭右开即[left, right)
while (left < right),这里使用 <
if (nums[middle] > target) right 更新为 middle
两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组
for (int i = 0; i < size; i++) { ? ?if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位 ? ? ? for (int j = i + 1; j < size; j++) { ? ? ? ? ? ? nums[j - 1] = nums[j]; ? ? ? } ? ? ? ? ? ? ? ?i--; // 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位 ? ? ? ? ? ? ? ?size--; // 此时数组的大小-1 ? ? } ? }
通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针
快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
慢指针:指向更新 新数组下标的位置
int slowIndex = 0; ? ? ? ?for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) { ? ? ? ? ? ?if (val != nums[fastIndex]) { ? ? ? ? ? ? ? ?nums[slowIndex++] = nums[fastIndex]; ? ? ? ? ? } ? ? ? }
每个数平方之后,排个序
最大值就在数组的两端,不是最左边就是最右边
i指向起始位置,j指向终止位置
新数组result,和A一样大,让k指向新数组终止位置
int k = A.size() - 1; ? ? ? ?vector<int> result(A.size(), 0); ? ? ? ?for (int i = 0, j = A.size() - 1; i <= j;) { // 注意这里要i <= j,因为最后要处理两个元素 ? ? ? ? ? ?if (A[i] * A[i] < A[j] * A[j]) { ? ? ? ? ? ? ? ?result[k--] = A[j] * A[j]; ? ? ? ? ? ? ? ?j--; ? ? ? ? ? } ? ? ? ? ? ?else { ? ? ? ? ? ? ? ?result[k--] = A[i] * A[i]; ? ? ? ? ? ? ? ?i++; ? ? ? ? ? } ? ? ? }
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
两个for循环,然后不断的寻找符合条件的子序列
一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置
不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果
int result = INT32_MAX; ? ? ? ?int sum = 0; // 滑动窗口数值之和 ? ? ? ?int i = 0; // 滑动窗口起始位置 ? ? ? ?int subLength = 0; // 滑动窗口的长度 ? ? ? ?for (int j = 0; j < nums.size(); j++) { ? ? ? ? ? ?sum += nums[j]; ? ? ? ? ? ?// 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件 ? ? ? ? ? ?while (sum >= s) { ? ? ? ? ? ? ? ?subLength = (j - i + 1); // 取子序列的长度 ? ? ? ? ? ? ? ?result = result < subLength ? result : subLength; ? ? ? ? ? ? ? ?sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置) ? ? ? ? ? } ? ? ? }
给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
模拟顺时针画矩阵的过程:
填充上行从左到右
填充右列从上到下
填充下行从右到左
填充左列从下到上
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组 ? ? ? ?int startx = 0, starty = 0; // 定义每循环一个圈的起始位置 ? ? ? ?int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理 ? ? ? ?int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2) ? ? ? ?int count = 1; // 用来给矩阵中每一个空格赋值 ? ? ? ?int offset = 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 += 1; ? ? ? } ? ? ? ? ?// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值 ? ? ? ?if (n % 2) { ? ? ? ? ? ?res[mid][mid] = count; ? ? ? } ? ? ? ?return res; ? }
通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域,最后一个节点的指针域指向null(空指针的意思)。单链表中的指针域只能指向节点的下一个节点
每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
循环链表,顾名思义,就是链表首尾相连。
// 单链表 struct ListNode { ? ?int val; ?// 节点上存储的元素 ? ?ListNode *next; ?// 指向下一个节点的指针 ? ?ListNode(int x) : val(x), next(NULL) {} ?// 节点的构造函数 };
删除链表中等于给定值 val 的所有节点。
直接使用原来的链表来进行删除操作。(单独写一段逻辑来处理移除头结点的情况)
设置一个虚拟头结点在进行删除操作。
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; ? }
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; ? }
// 定义链表节点结构体 struct LinkedNode { int val; LinkedNode* next; LinkedNode(int val):val(val), next(nullptr){} }; // 初始化链表 MyLinkedList() { _dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点 _size = 0; } // 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点 int get(int index) { if (index > (_size - 1) || index < 0) { return -1; } LinkedNode* cur = _dummyHead->next; while(index--){ // 如果--index 就会陷入死循环 cur = cur->next; } return cur->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* cur = _dummyHead; while(cur->next != nullptr){ cur = cur->next; } cur->next = newNode; _size++; } // 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。 // 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点 // 如果index大于链表的长度,则返回空 // 如果index小于0,则在头部插入节点 void addAtIndex(int index, int val) { if(index > _size) return; if(index < 0) index = 0; LinkedNode* newNode = new LinkedNode(val); LinkedNode* cur = _dummyHead; while(index--) { cur = cur->next; } newNode->next = cur->next; cur->next = newNode; _size++; } // 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的 void deleteAtIndex(int index) { if (index >= _size || index < 0) { return; } LinkedNode* cur = _dummyHead; while(index--) { cur = cur ->next; } LinkedNode* tmp = cur->next; cur->next = cur->next->next; delete tmp; //delete命令指示释放了tmp指针原本所指的那部分内存, //被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后, //如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针 //如果之后的程序不小心使用了tmp,会指向难以预想的内存空间 tmp=nullptr; _size--; } // 打印链表 void printLinkedList() { LinkedNode* cur = _dummyHead; while (cur->next != nullptr) { cout << cur->next->val << " "; cur = cur->next; } cout << endl; }
只需要改变链表的next指针的指向,直接将链表反转
定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
ListNode* reverseList(ListNode* head) { ListNode* temp; // 保存cur的下一个节点 ListNode* cur = head; ListNode* pre = NULL; while(cur) { temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next cur->next = pre; // 翻转操作 // 更新pre 和 cur指针 pre = cur; cur = temp; } return pre; }
ListNode* reverse(ListNode* pre,ListNode* cur){ if(cur == NULL) return pre; ListNode* temp = cur->next; cur->next = pre; // 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步 // pre = cur; // cur = temp; return reverse(cur,temp); } ListNode* reverseList(ListNode* head) { // 和双指针法初始化是一样的逻辑 // ListNode* cur = head; // ListNode* pre = NULL; return reverse(NULL, head); }
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
ListNode* swapPairs(ListNode* head) { ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点 dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作 ListNode* cur = dummyHead; while(cur->next != nullptr && cur->next->next != nullptr) { ListNode* tmp = cur->next; // 记录临时节点 ListNode* tmp1 = cur->next->next->next; // 记录临时节点 cur->next = cur->next->next; // 步骤一 cur->next->next = tmp; // 步骤二 cur->next->next->next = tmp1; // 步骤三 cur = cur->next->next; // cur移动两位,准备下一轮交换 } return dummyHead->next; }
如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。
ListNode* removeNthFromEnd(ListNode* head, int n) { ListNode* dummyHead = new ListNode(0); dummyHead->next = head; ListNode* slow = dummyHead; ListNode* fast = dummyHead; while(n-- && fast != NULL) { fast = fast->next; } fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点 while (fast != NULL) { fast = fast->next; slow = slow->next; } slow->next = slow->next->next; // ListNode *tmp = slow->next; C++释放内存的逻辑 // slow->next = tmp->next; // delete nth; return dummyHead->next; }
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
指针相等
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; }
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
判断链表是否环
分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇
如果有环,如何找到这个环的入口
index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。
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; }
哈希表是根据关键码的值而直接进行访问的数据结构。一般哈希表都是用来快速判断一个元素是否出现集合里。
牺牲了空间换取了时间
a和b都映射到了索引下标 1 的位置
发生冲突的元素都被存储在链表中
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
数组
set (集合)
map(映射)
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
两层for循环,同时还要记录字符是否重复出现
只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。
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; }
给定两个数组,编写一个函数来计算它们的交集。
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) { unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重 unordered_set<int> 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<int>(result_set.begin(), result_set.end()); }
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止
// 取数值各个位上的单数之和 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; } // 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false if (set.find(sum) != set.end()) { return false; } else { set.insert(sum); } n = sum; } }
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
这道题目中并不需要key有序,选择std::unordered_map 效率更高
vector<int> twoSum(vector<int>& nums, int target) { std::unordered_map <int,int> map; for(int i = 0; i < nums.size(); i++) { // 遍历当前元素,并在map中寻找是否有匹配的key auto iter = map.find(target - nums[i]); if(iter != map.end()) { return {iter->second, i}; } // 如果没找到匹配对,就把访问过的元素和下标加入到map中 map.insert(pair<int, int>(nums[i], i)); } return {}; }
给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。
为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) { unordered_map<int, int> 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; }
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
我们定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。
void reverseString(vector<char>& s) { for (int i = 0, j = s.size() - 1; i < s.size()/2; i++, j--) { swap(s[i],s[j]); } }
给定一个字符串 s 和一个整数 k,从字符串开头算起, 每计数至 2k 个字符,就反转这 2k 个字符中的前 k 个字符。
如果剩余字符少于 k 个,则将剩余字符全部反转。
如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。
遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。
因为要找的也就是每2 * k 区间的起点,这样写,程序会高效很多。
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.begin() + i, s.begin() + i + k ); } else { // 3. 剩余字符少于 k 个,则将剩余字符全部反转。 reverse(s.begin() + i, s.end()); } } return s; }
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
首先扩充数组到每个空格替换成"%20"之后的大小。
然后从后向前替换空格,也就是双指针法,过程如下:
i指向新长度的末尾,j指向旧长度的末尾。
很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
这么做有两个好处:
不用申请新数组。
从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。
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; }
给定一个字符串,逐个翻转字符串中的每个单词。
我们将整个字符串都反转过来,那么单词的顺序指定是倒序了,只不过单词本身也倒序了,那么再把单词反转一下,单词不就正过来了。
所以解题思路如下:
移除多余空格
将整个字符串反转
将每个单词反转
举个例子,源字符串为:"the sky is blue "
移除多余空格 : "the sky is blue"
字符串反转:"eulb si yks eht"
单词反转:"blue is sky the"
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; }
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
具体步骤为:
反转区间为前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; }
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。
前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。
然后针对前缀表到底要不要减一,这其实是不同KMP实现的方式
void getNext(int* next, const string& s) { int j = 0; next[0] = 0; for(int i = 1; i < s.size(); i++) { while (j > 0 && s[i] != s[j]) { j = next[j - 1]; } if (s[i] == s[j]) { j++; } next[i] = j; } } int strStr(string haystack, string needle) { if (needle.size() == 0) { return 0; } int next[needle.size()]; getNext(next, needle); int j = 0; for (int i = 0; i < haystack.size(); i++) { while(j > 0 && haystack[i] != needle[j]) { j = next[j - 1]; } if (haystack[i] == needle[j]) { j++; } if (j == needle.size() ) { return (i - needle.size() + 1); } } return -1; }
给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。
我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,要刨除 s + s 的首字符和尾字符,这样避免在s+s中搜索出原来的s,我们要搜索的是中间拼接出来的s
bool repeatedSubstringPattern(string s) { string t = s + s; t.erase(t.begin()); t.erase(t.end() - 1); // 掐头去尾 if (t.find(s) != std::string::npos) return true; // r return false; }
void getNext (int* next, const string& s){ next[0] = -1; int j = -1; for(int i = 1;i < s.size(); i++){ while(j >= 0 && s[i] != s[j + 1]) { j = next[j]; } if(s[i] == s[j + 1]) { j++; } next[i] = j; } } bool repeatedSubstringPattern (string s) { if (s.size() == 0) { return false; } int next[s.size()]; getNext(next, s); int len = s.size(); if (next[len - 1] != -1 && len % (len - (next[len - 1] + 1)) == 0) { return true; } return false; }
栈是先进后出,队列是先进先出。
C++中stack 是容器么?
我们使用的stack是属于哪个版本的STL?
我们使用的STL中stack是如何实现的?
stack 提供迭代器来遍历stack空间么?
栈和队列是STL(C++标准库)里面的两个数据结构
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)
我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。
deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。
SGI STL中 队列底层实现缺省情况下一样使用deque实现的。
我们也可以指定vector为栈的底层实现,初始化语句如下:
std::stack<int, std::vector<int> > third; // 使用vector为底层容器的栈
对应的队列的情况是一样的。
队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。
也可以指定list 为起底层实现,初始化queue的语句如下:
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列
所以STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)。
使用栈实现队列的下列操作:
push(x) -- 将一个元素放入队列的尾部。 pop() -- 从队列首部移除元素。 peek() -- 返回队列首部的元素。 empty() -- 返回队列是否为空。
需要两个栈一个输入栈,一个输出栈,这里要注意输入栈和输出栈的关系
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(x) -- 元素 x 入栈
pop() -- 移除栈顶元素
top() -- 获取栈顶元素
empty() -- 返回栈是否为空
用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。
queue<int> que1; queue<int> que2; // 辅助队列,用来备份 /** Initialize your data structure here. */ MyStack() { } /** Push element x onto stack. */ void push(int x) { que1.push(x); } /** Removes the element on top of the stack and returns that element. */ int pop() { int size = que1.size(); size--; while (size--) { // 将que1 导入que2,但要留下最后一个元素 que2.push(que1.front()); que1.pop(); } int result = que1.front(); // 留下的最后一个元素就是要返回的值 que1.pop(); que1 = que2; // 再将que2赋值给que1 while (!que2.empty()) { // 清空que2 que2.pop(); } return result; } /** Get the top element. */ int top() { return que1.back(); } /** Returns whether the stack is empty. */ bool empty() { return que1.empty(); }
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
括号匹配是使用栈解决的经典问题。
第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false
第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false
第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false
bool isValid(string s) { if (s.size() % 2 != 0) return false; // 如果s的长度为奇数,一定不符合要求 stack<char> st; for (int i = 0; i < s.size(); i++) { if (s[i] == '(') st.push(')'); else if (s[i] == '{') st.push('}'); else if (s[i] == '[') st.push(']'); // 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false // 第二种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false else if (st.empty() || st.top() != s[i]) return false; else st.pop(); // st.top() 与 s[i]相等,栈弹出元素 } // 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true return st.empty(); }
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
string removeDuplicates(string S) { stack<char> st; for (char s : S) { if (st.empty() || s != st.top()) { st.push(s); } else { st.pop(); // s 与 st.top()相等的情况 } } string result = ""; while (!st.empty()) { // 将栈中元素放到result字符串汇总 result += st.top(); st.pop(); } reverse (result.begin(), result.end()); // 此时字符串需要反转一下 return result; }
根据 逆波兰表示法,求表达式的值。
有效的运算符包括 + , - , * , / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
整数除法只保留整数部分。 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
逆波兰表达式:是一种后缀表达式,所谓后缀就是指运算符写在后面
去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
适合用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。
栈与递归之间在某种程度上是可以转换的
int evalRPN(vector<string>& tokens) { // 力扣修改了后台测试数据,需要用longlong 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.push(stoll(tokens[i])); } } int result = st.top(); st.pop(); // 把栈里最后一个元素弹出(其实不弹出也没事) return result; }
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来实现一个单调队列
deque<int> que; // 使用deque来实现单调队列 // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。 // 同时pop之前判断队列当前是否为空。 void pop(int value) { if (!que.empty() && value == que.front()) { que.pop_front(); } } // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。 // 这样就保持了队列里的数值是单调从大到小的了。 void push(int value) { while (!que.empty() && value > que.back()) { que.pop_back(); } que.push_back(value); } // 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。 int front() { return que.front(); }
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
优先级队列呢就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。
所以我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。
// 小顶堆 class mycomparison { public: bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) { return lhs.second > rhs.second; } }; vector<int> topKFrequent(vector<int>& nums, int k) { // 要统计元素出现频率 unordered_map<int, int> map; // map<nums[i],对应出现的次数> for (int i = 0; i < nums.size(); i++) { map[nums[i]]++; } // 对频率排序 // 定义一个小顶堆,大小为k priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que; // 用固定大小为k的小顶堆,扫面所有频率的数值 for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) { pri_que.push(*it); if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k pri_que.pop(); } } // 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组 vector<int> result(k); for (int i = k - 1; i >= 0; i--) { result[i] = pri_que.top().first; pri_que.pop(); } return result; }
面试题:栈里面的元素在内存中是连续分布的么?
这个问题有两个陷阱:
陷阱1:栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中是不是连续分布。
陷阱2:缺省情况下,默认底层容器是deque,那么deque的在内存中的数据分布是什么样的呢? 答案是:不连续的,下文也会提到deque。
请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
单调栈主要有三个判断条件。
当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况
当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况
当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况
vector<int> dailyTemperatures(vector<int>& T) { // 递增栈 stack<int> st; vector<int> result(T.size(), 0); st.push(0); for (int i = 1; i < T.size(); i++) { if (T[i] < T[st.top()]) { // 情况一 st.push(i); } else if (T[i] == T[st.top()]) { // 情况二 st.push(i); } else { while (!st.empty() && T[i] > T[st.top()]) { // 情况三 result[st.top()] = i - st.top(); st.pop(); } st.push(i); } } return result; }
给你两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。
请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。
nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
减栈就是求右边第一个比自己小的元素了。
三种情况,一定要分析清楚。
情况一:当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况
此时满足递增栈(栈头到栈底的顺序),所以直接入栈。
情况二:当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况
如果相等的话,依然直接入栈,因为我们要求的是右边第一个比自己大的元素,而不是大于等于!
情况三:当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况
此时如果入栈就不满足递增栈了,这也是找到右边第一个比自己大的元素的时候。
判断栈顶元素是否在nums1里出现过,(注意栈里的元素是nums2的元素),如果出现过,开始记录结果。
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) { stack<int> st; vector<int> result(nums1.size(), -1); if (nums1.size() == 0) return result; unordered_map<int, int> umap; // key:下标元素,value:下标 for (int i = 0; i < nums1.size(); i++) { umap[nums1[i]] = i; } st.push(0); for (int i = 1; i < nums2.size(); i++) { if (nums2[i] < nums2[st.top()]) { // 情况一 st.push(i); } else if (nums2[i] == nums2[st.top()]) { // 情况二 st.push(i); } else { // 情况三 while (!st.empty() && nums2[i] > nums2[st.top()]) { if (umap.count(nums2[st.top()]) > 0) { // 看map里是否存在这个元素 int index = umap[nums2[st.top()]]; // 根据map找到nums2[st.top()] 在 nums1中的下标 result[index] = nums2[i]; } st.pop(); } st.push(i); } } return result; }
给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。
vector<int> nextGreaterElements(vector<int>& nums) { // 拼接一个新的nums vector<int> nums1(nums.begin(), nums.end()); nums.insert(nums.end(), nums1.begin(), nums1.end()); // 用新的nums大小来初始化result vector<int> result(nums.size(), -1); if (nums.size() == 0) return result; // 开始单调栈 stack<int> st; st.push(0); for (int i = 1; i < nums.size(); i++) { if (nums[i] < nums[st.top()]) st.push(i); else if (nums[i] == nums[st.top()]) st.push(i); else { while (!st.empty() && nums[i] > nums[st.top()]) { result[st.top()] = nums[i]; st.pop(); } st.push(i); } } // 最后再把结果集即result数组resize到原数组大小 result.resize(nums.size() / 2); return result; }
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水
int trap(vector<int>& height) { int sum = 0; for (int i = 0; i < height.size(); i++) { // 第一个柱子和最后一个柱子不接雨水 if (i == 0 || i == height.size() - 1) continue; int rHeight = height[i]; // 记录右边柱子的最高高度 int lHeight = height[i]; // 记录左边柱子的最高高度 for (int r = i + 1; r < height.size(); r++) { if (height[r] > rHeight) rHeight = height[r]; } for (int l = i - 1; l >= 0; l--) { if (height[l] > lHeight) lHeight = height[l]; } int h = min(lHeight, rHeight) - height[i]; if (h > 0) sum += h; } return sum; }
int trap(vector<int>& height) { if (height.size() <= 2) return 0; vector<int> maxLeft(height.size(), 0); vector<int> maxRight(height.size(), 0); int size = maxRight.size(); // 记录每个柱子左边柱子最大高度 maxLeft[0] = height[0]; for (int i = 1; i < size; i++) { maxLeft[i] = max(height[i], maxLeft[i - 1]); } // 记录每个柱子右边柱子最大高度 maxRight[size - 1] = height[size - 1]; for (int i = size - 2; i >= 0; i--) { maxRight[i] = max(height[i], maxRight[i + 1]); } // 求和 int sum = 0; for (int i = 0; i < size; i++) { int count = min(maxLeft[i], maxRight[i]) - height[i]; if (count > 0) sum += count; } return sum; }
单调栈就是保持栈内元素有序。
情况一:当前遍历的元素(柱子)高度小于栈顶元素的高度 height[i] < height[st.top()]
情况二:当前遍历的元素(柱子)高度等于栈顶元素的高度 height[i] == height[st.top()]
情况三:当前遍历的元素(柱子)高度大于栈顶元素的高度 height[i] > height[st.top()]
int trap(vector<int>& height) { if (height.size() <= 2) return 0; // 可以不加 stack<int> st; // 存着下标,计算的时候用下标对应的柱子高度 st.push(0); int sum = 0; for (int i = 1; i < height.size(); i++) { if (height[i] < height[st.top()]) { // 情况一 st.push(i); } if (height[i] == height[st.top()]) { // 情况二 st.pop(); // 其实这一句可以不加,效果是一样的,但处理相同的情况的思路却变了。 st.push(i); } else { // 情况三 while (!st.empty() && height[i] > height[st.top()]) { // 注意这里是while int mid = st.top(); st.pop(); if (!st.empty()) { int h = min(height[st.top()], height[i]) - height[mid]; int w = i - st.top() - 1; // 注意减一,只求中间宽度 sum += h * w; } } st.push(i); } } return sum; }
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
int largestRectangleArea(vector<int>& heights) { int sum = 0; for (int i = 0; i < heights.size(); i++) { int left = i; int right = i; for (; left >= 0; left--) { if (heights[left] < heights[i]) break; } for (; right < heights.size(); right++) { if (heights[right] < heights[i]) break; } int w = right - left - 1; int h = heights[i]; sum = max(sum, w * h); } return sum; }
记录记录每个柱子 左边第一个小于该柱子的下标,而不是左边第一个小于该柱子的高度
int largestRectangleArea(vector<int>& heights) { vector<int> minLeftIndex(heights.size()); vector<int> minRightIndex(heights.size()); int size = heights.size(); // 记录每个柱子 左边第一个小于该柱子的下标 minLeftIndex[0] = -1; // 注意这里初始化,防止下面while死循环 for (int i = 1; i < size; i++) { int t = i - 1; // 这里不是用if,而是不断向左寻找的过程 while (t >= 0 && heights[t] >= heights[i]) t = minLeftIndex[t]; minLeftIndex[i] = t; } // 记录每个柱子 右边第一个小于该柱子的下标 minRightIndex[size - 1] = size; // 注意这里初始化,防止下面while死循环 for (int i = size - 2; i >= 0; i--) { int t = i + 1; // 这里不是用if,而是不断向右寻找的过程 while (t < size && heights[t] >= heights[i]) t = minRightIndex[t]; minRightIndex[i] = t; } // 求和 int result = 0; for (int i = 0; i < size; i++) { int sum = heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1); result = max(sum, result); } return result; }
栈顶和栈顶的下一个元素以及要入栈的三个元素组成了我们要求最大面积的高度和宽度
三种情况:
情况一:当前遍历的元素heights[i]大于栈顶元素heights[st.top()]的情况
情况二:当前遍历的元素heights[i]等于栈顶元素heights[st.top()]的情况
情况三:当前遍历的元素heights[i]小于栈顶元素heights[st.top()]的情况
int largestRectangleArea(vector<int>& heights) { int result = 0; stack<int> st; heights.insert(heights.begin(), 0); // 数组头部加入元素0 heights.push_back(0); // 数组尾部加入元素0 st.push(0); // 第一个元素已经入栈,从下标1开始 for (int i = 1; i < heights.size(); i++) { if (heights[i] > heights[st.top()]) { // 情况一 st.push(i); } else if (heights[i] == heights[st.top()]) { // 情况二 st.pop(); // 这个可以加,可以不加,效果一样,思路不同 st.push(i); } else { // 情况三 while (!st.empty() && heights[i] < heights[st.top()]) { // 注意是while int mid = st.top(); st.pop(); if (!st.empty()) { int left = st.top(); int right = i; int w = right - left - 1; int h = heights[mid]; result = max(result, w * h); } } st.push(i); } } return result; }