第一周:链表、栈、队列
1. 数组
数组操作时间复杂度:
- Look up >> O(1)
- Insert >> O(n)
- Delete >> O(n)
- Append (push back) >> O(1)
- Prepend (push front) >> O(n)
vector可变长动态数组
特点:
- 连续存储,空间可动态扩展
- 随机访问
[]
或vector.at()
- 节省空间,但通常并不是满存
- 内部插入、删除操作效率低,基本禁止。Vector 被设计成只能在后端进行追加和删除操作,其原因是vector 内部的实现是按照顺序表的原理,即只能push back 和pop back ,不能push front, pop front ;
- 当动态添加的数据超过vector 默认分配的大小时要进行内存的重新分配、拷贝与释放,这个操作非常消耗性能。 所以要vector 达到最优的性能,最好在创建vector 时就指定其空间大小。
如何实现一个变长数组?
- 分配多长的连续空间?
- 空数组,分配常数空间
- Push back空间不够用怎么处理?
- 申请2倍大小连续空间 >> 拷贝 >> 销毁原对象 >> 释放旧空间
- Pop back空间剩余超过75%怎么处理?
- 释放一半的空间
时间复杂度:
- 均摊O(1)
- 插入、删除O(n)
思考:若释放空间阀值设为50%,会怎么样
- 当空间占用为临界阀值附近时,会在扩容和减半操作之间来回震荡
声明初始化一个vector
#include<vector>
vector<int> vec; //声明一个int型向量
vector<int> vec(5); //声明一个初始大小为5的int向量
vector<int> vec(10, 1); //声明一个初始大小为10且值都是1的向量
vector<int> vec(tmp); //声明并用tmp向量初始化vec向量
vector<int> tmp(vec.begin(), vec.begin() + 3); //用向量vec的第0个到第2个值初始化tmp
int arr[5] = {1, 2, 3, 4, 5};
vector<int> vec(arr, arr + 5); //将arr数组的元素用于初始化vec向量
//说明:当然不包括arr[4]元素,末尾指针都是指结束元素的下一个元素,
//这个主要是为了和vec.end()指针统一。
vector<int> vec(&arr[1], &arr[4]); //将arr[1]~arr[4]范围内的元素作为vec的初始值
基本操作
(1). 容量
- 向量大小: vec.size();
- 向量最大容量: vec.max_size();
- 更改向量大小: vec.resize();
- 向量真实大小: vec.capacity();
- 向量判空: vec.empty();
- 减少向量大小到满足元素所占存储空间的大小: vec.shrink_to_fit(); //shrink_to_fit
(2). 修改
- 多个元素赋值: vec.assign(); //类似于初始化时用数组进行赋值
- 末尾添加元素: vec.push_back();
- 末尾删除元素: vec.pop_back();
- 任意位置插入元素: vec.insert();
- 任意位置删除元素: vec.erase();
- 交换两个向量的元素: vec.swap();
- 清空向量元素: vec.clear();
(3)迭代器
- 开始指针:vec.begin();
- 末尾指针:vec.end(); //指向最后一个元素的下一个位置
- 指向常量的开始指针: vec.cbegin(); //意思就是不能通过这个指针来修改所指的内容,但还是可以通过其他方式修改的,而且指针也是可以移动的。
- 指向常量的末尾指针: vec.cend();
(4)元素的访问
- 下标访问: vec[1]; //并不会检查是否越界
- at方法访问: vec.at(1); //以上两者的区别就是at会检查是否越界,是则抛出out of range异常
- 访问第一个元素: vec.front();
- 访问最后一个元素: vec.back();
- 返回一个指针: int* p = vec.data(); //可行的原因在于vector在内存中就是一个连续存储的数组,所以可以返回一个指针指向这个数组。这是是C++11的特性。
算法
// 遍历
vector<int>::iterator it;
for (it = vec.begin(); it != vec.end(); it++)
cout << *it << endl; // meanue1
for (size_t i = 0; i < vec.size(); i++) {
cout << vec.at(i) << endl; // meanuel2
}
// 元素翻转
#include <algorithm>
reverse(vec.begin(), vec.end());
// 元素排序
#include <algorithm>
sort(vec.begin(), vec.end()); //采用的是从小到大的排序
//如果想从大到小排序,可以采用上面反转函数,也可以采用下面方法:
bool Comp(const int& a, const int& b) {
return a > b;
}
sort(vec.begin(), vec.end(), Comp);
2. 链表
知识点简单,主要是多练习熟悉一下。
数组插入删除很贵O(n),引入链表
首先加入保护节点,哨兵节点
ListNode protect = new ListNode(0, head); // (value, ->next)
双向链表,双保护节
时间复杂度:
- look up >> O(n)
- Insert >> O(1)
- Delete >> O(1)
- Append(push back) >> O(1)
- Prepend(push front) >> O(1)
3. 栈stack
时间复杂度
- push 入栈 : O(1)
- pop 出栈 : O(1)
- Access 访问栈顶 : O(1)
只能访问、push、pop顶部的元素
操作
- top():返回一个栈顶元素的引用,类型为 T&。如果栈为空,返回值未定义。
- push(const T& obj):可以将对象副本压入栈顶。这是通过调用底层容器的 push_back() 函数完成的。
- push(T&& obj):以移动对象的方式将对象压入栈顶。这是通过调用底层容器的有右值引用参数的 push_back() 函数完成的。
- pop():弹出栈顶元素。
- size():返回栈中元素的个数。
- empty():在栈中没有元素的情况下返回 true。
- emplace():用传入的参数调用构造函数,在栈顶生成对象。
- swap(stack & other_stack):将当前栈中的元素和参数中的元素交换。参数所包含元素的类型必须和当前栈的相同。对于 stack 对象有一个特例化的全局函数 swap() 可以使用。
#include<stack>
#include<iostream>
stack<int> mystack1;
stack<int, vector<int> > mystack2;
4. 队列queue
时间复杂度
- push 入队 : O(1)
- pop 出队 : O(1)
- Access 访问队头 : O(1)
#include<queue>
#include<iostream>
queue<int> m;
queue<int, vector<int> > n;
操作
- front():返回 queue 中第一个元素的引用。如果 queue 是常量,就返回一个常引用;如果 queue 为空,返回值是未定义的。
- back():返回 queue 中最后一个元素的引用。如果 queue 是常量,就返回一个常引用;如果 queue 为空,返回值是未定义的。
- push(const T& obj):在 queue 的尾部添加一个元素的副本。这是通过调用底层容器的成员函数 push_back() 来完成的。
- push(T&& obj):以移动的方式在 queue 的尾部添加元素。这是通过调用底层容器的具有右值引用参数的成员函数 push_back() 来完成的。
- pop():删除 queue 中的第一个元素。
- size():返回 queue 中元素的个数。
- empty():如果 queue 中没有元素的话,返回 true。
- emplace():用传给 emplace() 的参数调用 T 的构造函数,在 queue 的尾部生成对象。
- swap(queue &other_q):将当前 queue 中的元素和参数 queue 中的元素交换。它们需要包含相同类型的元素。也可以调用全局函数模板 swap() 来完成同样的操作。
5. 双端队列deque
时间复杂度O(1)
![img](https://i-blog.csdnimg.cn/blog_migrate/b180b38f016683ea3870855a7566e8ab.jpeg)
一般队列以“时间”为顺序(先进先出)
优先队列按照元素的“优先级”取出
- ”优先级“可以使自己定义的一个元素属性
许多数据结构可以用来实现优先队列,例如二叉堆、二叉平衡树等
deque的API
-
1.1deque构造函数
deque deqT;//默认构造形式deque(beg, end);//构造函数将[beg, end)区间中的元素拷贝给本身。
deque(n, elem);//构造函数将n个elem拷贝给本身。
deque(const deque &deq);//拷贝构造函数。
-
==1.2 deque赋值操作
assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。assign(n, elem);//将n个elem拷贝赋值给本身。
deque& operator=(const deque &deq); //重载等号操作符
swap(deq);// 将deq与本身的元素互换
-
1.3 deque大小操作
deque.size();//返回容器中元素的个数deque.empty();//判断容器是否为空
deque.resize(num);//重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
deque.resize(num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置,如果容器变短,则末尾超出容器长度的元素被删除。
//遍历容器
void printDeque(const deque<int> &d)
{
for(deque<int>::const_iterator it = d.begin(); it != d.end(); it++)
{
cout<<*it<<" ";
}
cout<<endl;
}
2.1 deque双端插入和删除操作
push_back(elem);//在容器尾部添加一个数据
push_front(elem);//在容器头部插入一个数据
pop_back();//删除容器最后一个数据
pop_front();//删除容器第一个数据
2.2 deque数据存取
at(idx);//返回索引idx所指的数据,如果idx越界,抛出out_of_range。
operator[];//返回索引idx所指的数据,如果idx越界,不抛出异常,直接出错。
front();//返回第一个数据。
back();//返回最后一个数据
2.3 deque插入操作
insert(pos,elem);//在pos位置插入一个elem元素的拷贝,返回新数据的位置。
insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。
insert(pos,beg,end);//在pos位置插入[beg,end)区间的数据,无返回值。
2.4 deque删除操作
clear();//移除容器的所有数据
erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos);//删除pos位置的数据,返回下一个数据的位置。
6. 优先队列priority queue
时间复杂度
-
访问最值:O(1)
-
插入:一般是O(logN)
-
去最值:O(logN)
定义:priority_queue<Type, Container, Functional>
Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式,当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆
// 升序队列
priority_queue <int,vector<int>,greater<int> > q;
// 降序队列
priority_queue <int,vector<int>,less<int> >q;
// greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)
//对于基础类型 默认是大顶堆
priority_queue<int> a;
//等同于 priority_queue<int, vector<int>, less<int> > a;
和队列基本操作相同:
- top 访问队头元素
- empty 队列是否为空
- size 返回队列内元素个数
- push 插入元素到队尾 (并排序)
- emplace 原地构造一个元素并插入队列
- pop 弹出队头元素
- swap 交换内容
实战
数组
-
88.合并有序数组 easy
https://leetcode-cn.com/problems/merge-sorted-array/-
两个数组从后往前的指针开始比较,谁大放谁,放入后并----相应的指针
-
防止被覆盖,从后开始放最大的
-
边界问题
//一个数什么时候要的模板(重复) int n = 0; for (int i = 0; i < nums.size(); i++){ if (i == 0 || nusm[i] != nums[i - 1]){ nums[n] = nums[i]; n++; } }
-
-
26.去重 easy
https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/- 前后指针
- 建立循环, 前指针每次都++:
- 当前指针=后指针,后指针不变
- 前指针!=后指针,后指针 = 前指针,后指针++
- 边界问题,注意考虑前后指针都为0的情况
-
283.移动零 easy
https://leetcode-cn.com/problems/move-zeroes/- 建立新指针,从前往后遍历,不是0就要,指针++
- 如果n小于nums.size(),后面补0;
- 第二种思路:
- 双指针,快指针每次都++
- 当快指针扫描到非0,慢指针扫描到0时,交换位置
链表
-
临值查找 hard
https://www.acwing.com/problem/content/description/138/ -
141.环形链表 easy
https://leetcode-cn.com/problems/linked-list-cycle/- 快慢指针
- 相遇了则返回有环
- 不相遇返回无环
-
142.环形链表II mid
https://leetcode-cn.com/problems/linked-list-cycle-ii/- 根据:
fast = 2s (快指针每次2步,路程刚好2倍)
fast = s + nb (相遇时,刚好多走了n圈)
推出:slow = nb
1.第一次相遇,slow = nb
2.入环点 = a + nb = slow + a
所以说想再次到入环点,slow指针还要走a。
如何知道slow刚好走了a步? 从head开始,和slow指针一起走,相遇时刚好就是a步
- 根据:
-
206.反转链表 easy
https://leetcode-cn.com/problems/reverse-linked-list/-
注意最后的指向null的指针
-
next指针改为指向last的,但是链表操作中没有last指针,开一个新的指针记录last
-
最后返回的是指针
-
//遍历链表: while (head != nullptr){ head = head->next // 如果需要更改,新建一个指针暂存 }
-
-
25.K个一组翻转链表 hard
https://leetcode-cn.com/problems/reverse-nodes-in-k-group/-
分组,找到每一组的开始、结尾。按组遍历
-
// 找到一组k的结尾 private ListNode getEnd(ListNode head, intk){ while (head != null){ k--; if (k == 0)break; return head; } }
-
反转一组
-
// 反转两个节点之间的链表 private void reverseList(ListNode head, ListNode end){ if (head == end) return; ListNode last = head; head = head->next // 改变每条边,访问链表 while (head != end){ ListNode nextHead = last; // 改变一条边 head->next = last; // last, head的地址向后移动一位 last = head; head = nextHead; } end->next = last; }
-
上一组跟本组的head(旧的end)建立联系
-
让本组的新结尾(end)跟下一组建立联系
-
边界问题
-
栈
-
20.有效的括号 easy
https://leetcode-cn.com/problems/valid-parentheses/- 建立一个栈
- 左括号push,右括号和top比较。不对应就false,对应就pop
- 注意if嵌套关系,和判断pop后的空栈情况
-
155.最小栈 medium
https://leetcode-cn.com/problems/min-stack/- 新建两个栈,一个正常栈操作,一个使用前缀最小值
- 前缀最小值:新开栈,遍历数组和栈顶比,小于栈顶就进栈,大于就下一个
-
150.逆波兰式 后缀表达式求值 medium
https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/- 新建栈
- 遍历数组,是数字进栈,是运算符连续pop两个数字进行运算
-
227 medium &224 hard 基本计算器 中缀表达式
https://leetcode-cn.com/problems/basic-calculator-ii/
https://leetcode-cn.com/problems/basic-calculator/-
// 字符串转数值模板 long long val = 0; for (char ch : s){ if (ch >='0' && ch <= '9'){ val = val * 10 + ch - '0'; } }
-
定义优先级
-
转后缀,再求解
-
![截屏2021-06-21 上午2.15.50](https://tva1.sinaimg.cn/large/008i3skNly1grpa9lqmv9j30qa0b4adm.jpg)
![截屏2021-06-21 上午2.16.27](https://tva1.sinaimg.cn/large/008i3skNly1grpaa6w420j30pi0ysq9l.jpg)
homework
-
21.合并两个有序链表 easy
https://leetcode-cn.com/problems/merge-two-sorted-lists/- 创建哨兵节点,和一个pre指针
- 遍历l1和l2直到有一个为nullptr
- l1和l2的头节点比较,pre->next指向较小的(假如为l1)
- l1递归,l1 = l1->next
- 将l1或者l2剩下的接到最后
-
66.加一 easy
https://leetcode-cn.com/problems/plus-one/- 从后往前遍历,分三种情况
-
- 最后一个不是9,直接最后一位+1,break返回
- 最后一个是9,变为0,继续往前扫描
- 直到第一个数,如果其为9,创建一个新vector长度+1,第一个数为1其余的都为0;不是9的话+1
-
641.设置循环双端队列 medium
https://leetcode-cn.com/problems/design-circular-deque/