双指针算法
基本逻辑
for (i=0,j=0;i<n;i++)
{
while(j<i&&check(i,j))
j++;
//接下来依据不同题的逻辑。
}
暴力做法
for(i=0;i<n;i++)
for(j=0;j<0;j++)
...
核心思想:
将暴力做法优化到O(n)。
例题:
给定一个长度为n的整数序列,请找出最长的不包含重复数的连续区间,输出它的长度。
#include<iostream>
using namespace std;
const int N=100010;
int a[N],b[N];//a[N]存储n个整数
//b[N]存储每个整数出现的次数
/*
Eg:5
1 2 2 3 5
↓
b[1]=1;
b[2]=2;
b[3]=1;
b[5]=5;
对应的就是b[a[0]]=1....b[a[4]]=5
*/
int main()
{
int n;
cin>>n;
int maxl=1;
for(int i=0;i<n;i++) cin>>a[i];
for(int i=0,j=0;i<n;i++)
{
//i为右指针,j为左指针
b[a[i]]++;//记录a[i]在[i,j]间出现的次数
while(b[a[i]]>1)//当a[i]在[i,j]间出现的次数>1时,说明有元素重复了
//左指针j开始移动,当移动到重复的元素时,重复的元素的出现次数就<1了
//循环结束,这时[i,j]区间的元素恢复无重复元素状态
b[a[j++]]--;
//最长的不包含重复数字的连续子序列的长度
//i-j+1就是[i,j]无重复元素区间的长度
maxl=max(maxl,i-j+1);//比较当前[i,j]无重复元素的长度与之前的maxl值,取最大
}
cout<<maxl;
return 0;
}
位运算
基本用途
(1)求n的二进制表示数中的第k位数字:n >> k &1
(2)返回n的二进制表示数中的最后一位1:lowbit(n) =n & -n
lowbit原理
根据计算机负数表示的特点,如一个数字原码是10001000,他的负数表示形势是补码,就是反码+1,反码是01110111,加一则是01111000,二者按位与得到了1000,就是我们想要的lowbit操作
例子
给定一个长度为n的数列,请你求出数列中每个数的二进制表示中1的个数。
#include<iostream>
using namespace std;
int lowbit(int x)
{
return x&(-x);
}
int main()
{
int n;
cin>>n;
while(n--){
int x;
cin>>x;
int res=0;
while(x) x-=lowbit(x),res++;
cout<<res<<' ';
}
return 0;
}
离散化
基本用途
将无限空间中的有限个体映射到有限的空间中,即数的大小可以无限大,但数的个数有限。通俗来说,就是在不改变数据相对大小的条件下,对数据进行相应的缩小。
基本模板
vector<int> alls;//存储所有待离散化的值
sort(alls.begin(),alls.end());将所有值排序
alls.erase(unique(alls.begin(),alls.end()),alls.end());//去掉重复元素
//二分求出x对应的离散化的值
int find(int x)//找到第一个大于等于x的位置
{
int l=0,r=alls.size()-1;
while(l<r)
{
int mid= l+r>>1;
if(alls[mid]>=x)
r=mid;
else
l=mid+1;
}
return r+1;//加上1就从1开始映射,不加就从0开始映射
}
例子
假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r]之间的所有数的和。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N=300010;//坐标x的数量上限为1e5,两个坐标l,r的数量上限也为1e5,所以加起来为3*le5;
typedef pair<int,int> PII;//pair<int,int>类似于结构体的简写版,typedef定义了一个新的类型PII(跟结构体定义了一个结构体类型然后使用相似)
int a[N],s[N];
vector<int> alls;
vector<PII> add,query;
//使用二分查找x所在的位置,此时是alls(x,l,r)排好序的,返回的坐标也会是按照x的大小所给出的;
int find(int x)
{
int l=0,r=alls.size()-1;
while(l<r)
{
int mid=(l+r)/2;
if(alls[mid]>=x) r=mid;
else l=mid+1;
}
return r+1;//因为后续要使用前缀和,所以返回的坐标要加上1;
}
int main()
{
int n,m;cin>>n>>m;
//分别将要操作的四组数据记录在add和query中,将l,r,x的坐标值保存在alls中;
for(int i=0;i<n;i++)
{
int x,c;
cin>>x>>c;
add.push_back({x,c});
alls.push_back(x);
}
for(int i=0;i<m;i++)
{
int l,r;
cin>>l>>r;
query.push_back({l,r});
alls.push_back(l);
alls.push_back(r);
}
//将alls进行排序,并将重复的操作删除掉(如进行了两次在x的增值操作,应该去掉一个x保持平衡);
sort(alls.begin(),alls.end());
alls.erase(unique(alls.begin(),alls.end()),alls.end());
//一个迭代器从1开始直到末尾结束,itdm.first是x,second是c(在上方循环中可知);
for(auto itdm : add)
{
int x;
x=find(itdm.first);
a[x]+=itdm.second;
}
//只循环x是因为x的坐标加上了值,而l,r可能有(l或r与x的值相同),且定义全局变量数组,其余值默认为0,故可以方便计算;
for(int i=1;i<=alls.size();i++) s[i]=s[i-1]+a[i];
for(auto itdm : query)
{
int l,r;
l=find(itdm.first);//找出l,r在a中的坐标
r=find(itdm.second);
printf("%d\n",s[r]-s[l-1]);//前缀和上方已计算,所以可直接输出,记得加上换行符!
}
return 0;
}
区间合并
基本用途
区间维护,将相交,向包含的区间合并。
代码模板
void merge(vector<PII> &segs)
{
vector<PII> res;
sort(segs.begin(), segs.end());
int st = -2e9, ed = -2e9;
for (auto seg : segs)
if (ed < seg.first)
{
if (st != -2e9) res.push_back({st, ed});
st = seg.first, ed = seg.second;
}
else ed = max(ed, seg.second);
if (st != -2e9) res.push_back({st, ed}); //这里的if就是判断是不是一组st和ed都没有输入进来
//在例题中因为保证n是≥1的,所以肯定不可能为空,就可以省去这一特判.
segs = res;
}
例题
给定 n个区间 [li,ri],要求合并所有有交集的区间。注意如果在端点处相交,也算有交集。输出合并完成后的区间个数。例如:[1,3]和 [2,6] 可以合并为一个区间 [1,6]。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int,int> PII;
void merge(vector<PII> &segs)
{
vector<PII> res; // 存储合并区间之后的数据对
sort(segs.begin(),segs.end()); // C++对pair容器排序规则是按照第一个数据排序
int st=-1e9-10,ed=-1e9-10;
// st和ed分别作为维护(被比较)区间左右端点,seg.first,seg.second分别作为当前区间左右端点
for(auto seg:segs) // 遍历segs中每一个元素,重复1,2
{
if(ed<seg.first) // 若维护区间和当前区间没有交集(维护区间右端点在当前区间左端点的左边)
{
if(st!=-1e9-10) res.push_back({st,ed});
// 1.此时若维护区间不是初始定义区间,说明维护区间可作为独立区间加入到res
// 把当前区间更新为维护区间,下一个区间与它做端值的比较
st=seg.first,ed=seg.second;
}
else ed=max(ed,seg.second);
// 2.不管是维护区间包含当前区间还是维护区间和当前区间有交集,
// 都把维护区间右端点更新为维护区间和当前区间右端点中更大的那一个
}
if(st!=-1e9-10) res.push_back({st,ed});
// 在for循环结束中最后一个区间只是更新成了维护区间,并没有把它加入到res中,现在把它也放进来
segs=res;
}
int main()
{
int n;
scanf("%d",&n);
vector<PII> segs; // 存储合并区间之前的数据对
while(n--)
{
int l,r;
scanf("%d%d",&l,&r);
segs.push_back({l,r});
}
merge(segs);
printf("%d",segs.size());
return 0;
}
单链表
单链表的定义
线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。**为了建立数据元素之间的线性关系,对每个链表结点,出存放元素自身信息外,还需要存放一个指向其后继的指针。
单链表基本操作的实现
1.定义单链表
typedef struct _LinkNode {
int data; //结点的数据域
struct _LinkNode* next;//结点的指针域
}LinkNode, Linklist;
2.单链表初始化
bool InitList(Linklist* &L) {
L = new LinkNode;
if (!L) return false;//生成结点失败
L->next = NULL;
return true;
}
3.头插法创建单链表
bool ListInsert_front(Linklist* &L, LinkNode* node) {
if (!L || !node) return false;
node->next = L->next;
L->next = node;
}
4.尾插法创建单链表
bool ListInsert_back(Linklist* &L, LinkNode* node) {
LinkNode* last = NULL;
if (!L || !node) return false;
last = L;
while (last->next) {
last = last->next;
}
node->next = NULL;
last->next = node;
return true;
}
5.任意位置插入
bool ListInsert(Linklist* &L,int i,int e) {
if (!L)return false;
int j = 0;
Linklist* p, * s;
p = L;
while (p&&j<i-1)//查找位置为i-1的结点,p指向该结点
{
p = p->next;
j++;
}
if (!p || j > i - 1) {
return false;
}
s = new LinkNode;//生成新结点
s->data = e;
s->next = p->next;
p->next = s;
}
6.按位取值
bool Link_GetElem(Linklist*& L, int i, int& e) {
//在带头节点的单链表L中查找第i个元素
//用e记录L中第i个元素的值
int index;
Linklist* p;
if (!L || !L->next) return false;
p = L->next;
index = 1;
while (p && index < i) {//链表向后扫描,直到p指向第i个元素或者p为空
p = p->next;
index++;
}
if (!p || index > i) return false;//i值不合法,i>n或i<=0;
e = p->data;
return true;
}
7.按值取位
bool Linklist_FindElement(Linklist* L,int e,int& index) {
//在带头结点的单链表L中查找值为e的元素位置
Linklist* p;
p = L->next;
index = 1;
if (!L || !L->next) {
index = -1;
return false;
}
while (p&&p->data!=e) {
p = p->next;
index++;
}
if (!p) {
index = -1;
return false;//查找结点不存在
}
return true;
}
8.删除节点
bool LinklistDelete(Linklist* L, int i) {
Linklist* p,*q;
p = L;
int index = 0;
if (!L || !L->next) return false;
while ((p->next) && (index < i - 1)) {
index++;
p = p->next;
}
if (!p->next || index > i - 1) return false;
q = p->next; //临时保存被删结点的地址以备释放空间
p->next = q->next; //改变删除结点前驱结点的指针域
delete q; //释放被删除结点的空间
return true;
}
9.打印链表
void LinkPrint(Linklist* &L) {
LinkNode* p = NULL;
if (!L) { cout << "此链表为空"<<endl; return; }
p = L->next;
while (p) {
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
10.销毁链表
void LinklistDestroy(Linklist*& L) {
Linklist* p=L;
cout << "销毁链表" << endl;
while (p) {
L = L->next; //L指向下一个结点
delete p; //删除当前结点
p = L; //p移向下一个结点
}
}
例子
实现一个单链表,链表初始为空,支持三种操作:
-
向链表头插入一个数;
-
删除第 k个插入的数后面的数;
-
在第 k 个插入的数后插入一个数。
现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。
注意:题目中第 k个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n个数,则按照插入的时间顺序,这 n 个数依次为:第 1个插入的数,第 2 个插入的数,…第 n 个插入的数。
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int h[N], e[N], ne[N], head, idx;
//对链表进行初始化
void init(){
head = -1;//最开始的时候,链表的头节点要指向-1,
//为的就是在后面进行不断操作后仍然可以知道链表是在什么时候结束
/*
插句题外话,我个人认为head其实就是一个指针,是一个特殊的指针罢了。
刚开始的时候它负责指向空结点,在链表里有元素的时候,它变成了一个指向第一个元素的指针
当它在初始化的时候指向-1,来表示链表离没有内容。
*/
idx = 0;//idx在我看来扮演两个角色,第一个是在一开始的时候,作为链表的下标,让我们好找
//第二在链表进行各种插入,删除等操作的时候,作为一个临时的辅助性的所要操作的元素的下
//标来帮助操作。并且是在每一次插入操作的时候,给插入元素一个下标,给他一个窝,感动!
/*
再次插句话,虽然我们在进行各种操作的时候,元素所在的下标看上去很乱,但是当我们访问的
时候,是靠着指针,也就是靠ne[]来访问的,这样下标乱,也就我们要做的事不相关了。
另外,我们遍历链表的时候也是这样,靠的是ne[]
*/
}
//将x插入到头节点上
void int_to_head(int x){//和链表中间插入的区别就在于它有head头节点
e[idx] = x;//第一步,先将值放进去
ne[idx] = head;//head作为一个指针指向空节点,现在ne[idx] = head;做这把交椅的人换了
//先在只是做到了第一步,将元素x的指针指向了head原本指向的
head = idx;//head现在表示指向第一个元素了,它不在是空指针了。(不指向空气了)
idx ++;//指针向下移一位,为下一次插入元素做准备。
}
//将x插入到下标为k的点的后面
void add(int k, int x){
e[idx] = x;//先将元素插进去
ne[idx] = ne[k];//让元素x配套的指针,指向它要占位的元素的下一个位置
ne[k] = idx;//让原来元素的指针指向自己
idx ++;//将idx向后挪
}
//将下标是k的点后面的点个删掉
void remove(int k){
ne[k] = ne[ne[k]];//让k的指针指向,k下一个人的下一个人,那中间的那位就被挤掉了。
}
int main(){
cin >> n;
init();//初始化
for (int i = 0; i < n; i ++ ) {
char s;
cin >> s;
if (s == 'H') {
int x;
cin >> x;
int_to_head(x);
}
if (s == 'D'){
int k;
cin >> k;
if (k == 0) head = ne[head];//删除头节点
else remove(k - 1);//注意删除第k个输入后面的数,那函数里放的是下标,k要减去1
}
if (s == 'I'){
int k, x;
cin >> k >> x;
add(k - 1, x);//同样的,第k个数,和下标不同,所以要减1
}
}
for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ' ;
cout << endl;
return 0;
}
欢迎订阅专栏,数据结构实验,期末大作业,前端后端,算法都有哦,想我发哪个方面的资源或文章可以私信我,免费的哦