链表、单调队列

title: 链表、单调队列
date: 2019-05-27 23:39:42
tags: 数据结构

一、链表与邻接表
链表
建链表的方式

1.指针方式

struct node{
	int val;
	node* next;
};

这种方式每次都要去new node();,速度很慢

2.数组方式 也称 静态链表
(1)用数组模拟单链表,用的最多的是 邻接表
邻接表最主要的应用就是用来存储 树和图

val[],next[]通过下标关联起来,最后一个结点的next值为 -1
例题:

#include <iostream>
#include <algorithm>
#include <vector>
const int maxn=1e5+5;
using namespace std;
int val[maxn],nex[maxn];//注意:有些编译器版本下 next[]可能会与保留字冲突
int head=-1,idx=0;//两个指针,head指向头结点,idx指向当前用到的结点,用于扩展链表 
//这个赋值表示初始时,head为空,idx可以从0处开始  尾插法 用一个指针tail记录就行 
//我原本想能不能不要head指针,默认头结点就是0下标处,
//但是 如果一旦删除了0下标处的那个结点,后面的数就成了头结点,而没有办法去人工改变 
void add(int x)//将x插到头结点 
{
	val[idx]=x;
	nex[idx]=head;
	head=idx;
	idx++;
}
void insert(int k,int x)
{
	val[idx]=x; 
	nex[idx]=nex[k];
	nex[k]=idx;
	idx++;
}
void delet(int k)
{
	nex[k]=nex[nex[k]];
}
int main()
{
    int n;
    scanf("%d",&n);
    char c;
    int x,k;
    while(n--)
    {
    	//scanf("%c",&c); 会吃到n后面那个空格 
    	cin>>c;//cin就不会...真是奇怪 
    	if(c=='H')
    	{ 
    		cin>>x;
    		add(x);
		}else if(c=='D')
		{
			cin>>k;
			//特别要注意特判一下删除的是头结点的情况
			if(k==0)
			head=nex[head];
			else 
			delet(k-1);
		}else
		{
			cin>>k>>x;
			insert(k-1,x);//注意,这里第k个数的下标实际是k-1,因为从0开始 
		}
	}
	for(int i=head;i!=-1;)
	{
		cout<<val[i]<<" ";
		i=nex[i];
	}
	cout<<endl;
    return 0;
}

(2)双链表,主要用来做优化

val[],l[],r[]

下图展示了在k结点右边插入一个结点的过程:

例题:

#include <iostream>
const int maxn=1e5+5;
using namespace std;
int n,idx;
int val[maxn],l[maxn],r[maxn];
void insert(int k,int x)
{
	val[idx]=x;
	l[idx]=k,r[idx]=r[k];
	l[r[k]]=idx;
	r[k]=idx;
	idx++;
}
void delet(int k)
{
	r[l[k]]=r[k];
	l[r[k]]=l[k];
}
int main()
{
	int n;
	scanf("%d",&n);
	
	//初始化,0是左端点,1是右端点
	r[0]=1,l[1]=0; //这里我们可以不必定义head和tail指针
	idx=2;
	while(n--)
	{
		string c;
		cin>>c;
		int k,x;
		if(c=="L")
		{
			cin>>x;
			insert(0,x);
		}else if(c=="R")
		{
			cin>>x;
			insert(l[1],x);//因为有双指针,所以只需要写一个在右边插入的函数即可 
		}else if(c=="D")
		{
			cin>>k;
			delet(k+1);//因为0,1已经占用了,是从2开始的 
		}else if(c=="IL")
		{
			cin>>k>>x;
			insert(l[k+1],x); 
		}else
		{
			cin>>k>>x;
			insert(k+1,x);
		}
	}
	
	for(int i=r[0];i!=1;i=r[i])
	cout<<val[i]<<" ";
	cout<<endl;
	
	return 0;
 } 
邻接表 其实就是多个单链表

把所有点的邻边都存下来
head[0] -> X -> X
head[1] -> X -> X
head[2] -> X -> X
其他操作同单链表的操作

二、单调栈与单调队列
单调栈
  • 单调递增栈:数据出栈的序列是单调递增序列

  • 单调递减栈:数据出栈的序列是单调递减序列

  • 实现:维护一个始终保持其中元素单调的栈,如果即将入栈的新元素会破坏单调性,就弹出旧元素,直到新元素不会破坏单调性。

  • 时间复杂度为O(n),每个元素最多进栈一次,出栈一次

应用举例:

(1)给定一组数,对于某个元素i

​ 1. 确定左边区间第一个比它小(或大)的数及到这个数之间有多少个数

​ 2. 确定它是否是区间最值,以及包含这个最值的最大区间长度

(2)给定一序列,寻找某一子序列,使得子序列中的最小值乘以子序列的长度最大

(3)给定一序列,寻找某一子序列,使得子序列中的最小值乘以子序列所有元素和最大

通用模板:

//常见模型:找出每个数左边离它最近的比它大/小的数
int cnt=0;
for (int i=1;i<=n;i++)
{
    while (cnt&&check(st[cnt],i)) 
     cnt--;
    st[++cnt]=i;
}

例题:
1.简单题
直接超简易版

	while(n--)
	{
        int x;
        scanf("%d",&x);
        while(tt&&st[cnt]>=x) //删除所有的逆序
        	cnt--;
        if(!cnt) 
         printf("-1 ");
        else 
         printf("%d ",st[cnt]);
        st[++cnt]=x;
    }

以下为我最开始写的版本

#include <iostream>
const int maxn=1e5+5;
using namespace std;
int a[maxn];
int cnt=0;
int main()
{
    int n;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        int x;
        cin>>x;
        while(cnt)
        {
            if(a[cnt]<x)
            {
                cout<<a[cnt]<<" ";
                a[++cnt]=x;
                break;
            }
            else
            {
                cnt--;
            }
        }
        if(!cnt)
        {
            a[++cnt]=x;
            cout<<"-1"<<" ";
        }
    }
    return 0;
}

注意:scanf 是最快的,大概比纯cin快10倍吧

常用的加速方法有:
cin.tie(0);   //更快
ios_sync_with_stdio(false);

2.HDU-1506 求最大矩形面积

单调栈解法:(当然也可以用单调队列)

我们知道单调栈可用于求出左边第一个比它小的数,所以对于矩形的每一个h[i],我们找到它左边和右边第一个比它小的矩形下标,作为分界,就可以求出矩形h[i]可向左右两边扩展的宽度,即(r[i]-l[i]-1)

#include <iostream>
#include <map>
#include <algorithm>
#include <cstring>
#include <queue>
#include <stack>
#include <cmath>
#include <stdlib.h>
#define ll long long
const int maxn=1e5+5;
const int inf=0x3f3f3f3f;
const double eps=1e-6;
using namespace std;
stack<ll>st; //存放下标 
ll h[maxn];
ll l[maxn];
ll r[maxn];
int main()
{
	int n;
	ll area;
	
	while(scanf("%d",&n)&&n)
	{
		area=0;
		for(int i=1;i<=n;i++) 
		scanf("%lld",&h[i]);
		ll t;
		h[n+1]=0;
		while(!st.empty())
        st.pop();
		for(int i=1;i<=n;i++)//找左边界 
        {
            while(!st.empty()&&h[st.top()]>=h[i]) 
			st.pop();
            if(st.empty()) 
			l[i]=0;//左边没有比它小的 
            else 
			l[i]=st.top();
            st.push(i);
         }
        	
		while(!st.empty())
        st.pop();
        for(int i=n;i>=1;i--)//找右边界 
		{
			while(!st.empty()&&h[st.top()]>=h[i]) 
			st.pop();
			if(st.empty()) 
			r[i]=n+1;//右边没有比它小的 
            else 
			r[i]=st.top();
            st.push(i);
		 } 
		 
		for(int i=1;i<=n;i++)
		area=max(area,(r[i]-l[i]-1)*h[i]);	
		printf("%lld\n",area);
	}
	return 0;
}

笛卡尔树写法:

#include <iostream>
#include <map>
#include <algorithm>
#include <cstring>
#include <queue>
#include <stack>
#include <cmath>
#include <stdlib.h>
#define ll long long
const int maxn=1e5+5;
const int inf=0x3f3f3f3f;
const double eps=1e-6;
using namespace std;
stack<ll>st; //存放下标 
ll h[maxn];
ll l[maxn];
ll r[maxn];
ll area;
struct node{
	ll index,val;
	node *parent,*lchild,*rchild;
	node(ll id=0,ll v=0,node* l=NULL,node* r=NULL)//注意应该是这样写构造函数的初始化 
	{
		index=id;
		val=v;
		lchild=l;
		rchild=r;
		}	
};
node* build(ll a[],int n)
{
	stack<node*>st;//用来维护极右链
	node *now,*temp,*last;
	 for(int i=1;i<=n;i++)
	 {
	 	now=new node(i,a[i]);
	 	last=NULL;
		
		while(!st.empty())
		{
			if(st.top()->val < now->val)
			{
				temp=st.top();
				if(temp->rchild)
				{
					temp->rchild->parent=now;
					now->lchild=temp->rchild; 
				}
				temp->rchild=now;
				now->parent=temp;
				break; 
			}
			
			last=st.top();
			st.pop();
		 } 
	 	
	 	if(st.empty()&&last)
		{ 					
			now->lchild=last;
			last->parent=now;
	 	}
	 	
	 	st.push(now);
	 }
	
	while(!st.empty()) 
	{
		temp=st.top();
		st.pop();
	}
	return temp;
}
int dfs(node *root)
{
	int cnt=1;
	if(root->lchild)
	cnt+=dfs(root->lchild);
	if(root->rchild)
	cnt+=dfs(root->rchild);
	area=max(area,(ll)cnt*root->val);
	return cnt;
}
int main()
{
	int n;
	while(scanf("%d",&n)&&n)
	{
		area=0;
		for(int i=1;i<=n;i++) 
		scanf("%lld",&h[i]);
		node *root;
		root=build(h,n);
		//cout<<root->val<<endl;
		dfs(root);
		printf("%lld\n",area);
	}
	return 0;
}
单调队列

注意有一个队列的大小问题,还要处理队满的情况。关于队列

同样可分为单调递增队列和单调递减队列。

  • 实现:(以单调递减队列为例),往队尾添加元素,如果原队尾元素小于当前元素,就出队,直到队尾元素比当前元素大,如果队满,就从队首删除元素。

    注意:一般的队列不支持队尾删除,所以一般用双端队列dequeue,但是用数组模拟就不存在这样的问题啦

  • 时间复杂度为O(n)

  • 应用举例:

    (1)可以查询区间最值(不能维护区间k大,因为队列中很有可能没有k个元素)

    (2)优化DP

常用模板:

//常见模型:找出滑动窗口中的最大值/最小值
int head=0,tail=-1;
for(int i=0;i<n;i++)
{
    while(head<=tail&&check_out(q[head])) 
    	head++;  // 判断队头是否滑出窗口
    while(head<=tail&&check(q[tail], i)) 
    	tail--;
    q[++tail]=i;
}

例题:


分析一下暴力做法的时间复杂度:
遍历一遍窗口,以寻找最大最小值 O(k)
对于每一个数有一个加入队列(同时可能有一个数出队)的过程 O(n)
总复杂度就为O(nk)    1 0 12 10^{12} 1012太大了

用单调队列优化一下:

不开 O 2 , O 3 O_2,O_3 O2,O3优化时,STL会比 数组模拟 稍慢,有些情况下做题可能会卡常

注意:求最大最小值是 分别用 一个单调队列做的,而不是用一个单调队列能够同时做出最大最小值

#include <iostream>
const int maxn=1e6+5;
using namespace std;
int a[maxn],q[maxn];//此时队列里存的应该时下标
int n,k; 
int main()
{
	scanf("%d%d",&n,&k);
	for(int i=0;i<n;i++)
	scanf("%d",&a[i]);
	
	//求最小值  队列中应该是单调增的 答案就是队首值 
	int h=0,t=-1;//注意一定要初始化,最开始队列为空
	for(int i=0;i<n;i++)
	{
		if(h<=t&&i-k+1>q[h])//判断队头有没有滑出窗口
			h++;//这里不用while是因为每次窗口只会往后移动一位,所以每次队列中最多只有一个数是不在窗口内的,
			//不知道多少次的一般情况下,是可以写while的
		while(h<=t&&a[q[t]]>=a[i])
		t--;//如果当前队列中的数都比现在要加入的数大,则出队
		q[++t]=i;//这里一定要注意,先入队再输出
		if(i>=k-1)//i从3开始输出 
		printf("%d ",a[q[h]]);	 
	 } 
	 //printf("\n");
	 puts("");//也可以 
	
	//求最大值
	h=0,t=-1;
	for(int i=0;i<n;i++)
	{
		if(h<=t&&i-k+1>q[h])//判断队头有没有滑出窗口
			h++;
		while(h<=t&&a[q[t]]<=a[i])
		t--;//如果当前队列中的数都比现在要加入的数小,则出队
		q[++t]=i;
		if(i>=k-1)//i从3开始输出 
		printf("%d ",a[q[h]]);	 
	 }  
	printf("\n");
	return 0;
}
三、笛卡尔树(Cartesian Tree)

是一种特定的二叉树结构,可由数列构造,从数列中构造一棵笛卡尔树可以线性时间完成

和二叉搜索树结构上是相同的

性质:
  • 结点唯一对应于数列元素

    即数列中的每个元素都对应于树中某个唯一结点,树结点也对应于数列中的某个唯一元素

  • 中序遍历笛卡尔树即可得到原数列

    即任意树结点的左子树结点所对应的数列元素下标比该结点所对应元素的下标小,右子树结点所对应数列元素下标比该结点所对应元素下标大。

  • 树结构存在堆序性质

即任意树结点所对应数值大/小于其左、右子树内任意结点对应数值

暴力建treap,复杂度可能是O(nlog n),而用笛卡尔树建,则是O(n)的

注意:笛卡尔树建树,插入的元素必须保证key值递增,而val值是可以乱

笛卡尔树的构造:(以小根堆为例)
  1. 用单调栈实时维护笛卡尔树的极右链,即根,根的右儿子,根的右儿子的右儿子…
  2. 栈底是根,这样很显然从栈顶到栈底,val值是不断变小的
  3. 当我们有一个新结点,我们从栈顶开始往下找,找到第一个结点的val值比我小,那么新结点必须得是它的儿子,且作为右儿子(因为晚进,它的下标必然是更大的),它原来的右儿子key值比新结点小,所以作为新结点的左儿子,并将它从极右链(栈维护的)中删除,将新结点加入栈中,这样就维护了一个treap的性质。
  4. 每个点这样进栈一次,出栈一次,复杂度就为O(n)
先看一个朴素的O(nlogn)的建树方法

其实接近于 O ( n 2 ) O(n^2) O(n2)

int build_tree(int l,int r)
//在l-r这个区间建立一颗笛卡尔树,返回根节点的标号
{
    if(l>r) return 0;//数组存储树的父子关系,0即NULL
    if(l==r) return l;
    int p=l,Min_val=a[p];
    //p记录区间最小值在原序列的下标,Min_val记录最小值, a数组存储原序列
    for(int i=l+1;i<=r;++i)
        if(a[i]<Min_val)
            p=i,Min_val=a[i];
    
    lc[p]=build_tree(l,p-1);//lc[i]:标号为i的节点的左子树根节点的标号
    rc[p]=build_tree(p+1,r);//rc[i]:标号为i的节点的右子树根节点的标号
    return p;
}
然后看O(n)的建树方法
#include <iostream>
#include <algorithm>
#include <cmath> 
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <queue>
#include <stack>
#define ll long long 
const int maxn=100010;
const int inf=0x3f3f3f3f;
const double eps=1e-8;
using namespace std;
ll mod=1000000007;
using namespace std;
struct node{
	int index,val;
	node *parent,*lchild,*rchild;
	node(int id=0,int v=0,node* l=NULL,node* r=NULL)//注意应该是这样写构造函数的初始化 
	{
		index=id;
		val=v;
		lchild=l;
		rchild=r;
		}	
};
node* build(int a[],int n)
{
	stack<node*>st;//用来维护极右链
	node *now,*temp,*last;
	 for(int i=1;i<=n;i++)
	 {
	 	now=new node(i,a[i]);//指向当前要插入的结点 
	 	//cout<<now->index<<"n"<<now->val<<endl;
	 	last=NULL;//指向最后被弹出的元素
		
		while(!st.empty())
		{
			if(st.top()->val < now->val)//找到栈中比当前结点值小的了,准备插入作为它的右儿子 
			{
				temp=st.top();//temp记录当前栈顶元素,即要插入结点的父结点
				if(temp->rchild)//如果父结点原来有右儿子,它应该变成插入结点的左儿子 
				{
					temp->rchild->parent=now;
					now->lchild=temp->rchild; 
				}
				//如果原结点没有右儿子 
				temp->rchild=now;
				now->parent=temp;
				break;//插入完成就跳出循环 
			}
			
			last=st.top();
			st.pop();//查找的时候这个极右链更大的元素就会被pop掉 
		 } 
	 	
	 	if(st.empty()&&last)//特判一种可能出现的情况,就是当前节点(最小,即将作为根节点)
		{ 					//把栈全部弹空了,就要把原先的根节点作为当前节点的左子节点
			now->lchild=last;
			last->parent=now;
	 	}
	 	
	 	st.push(now);//两种情况都要把新进入极右链的结点push进栈 
	 }
	
	while(!st.empty()) 
	{
		temp=st.top();
		st.pop();
	}//把根节点取出来return 
	
	//cout<<temp->index<<"*"<<temp->val<<endl;
	return temp;
}
void print(node *root)
{
	cout<<"index  "<<root->index<<" "<<"val  "<<root->val<<" ";
	if(root->parent)
	cout<<"father  "<<root->parent->val<<" ";
	if(root->lchild)
	cout<<"l  "<<root->lchild->val<<" ";
	if(root->rchild)
	cout<<"r  "<<root->rchild->val;
	cout<<endl;
	if(root->lchild)
	{
		print(root->lchild);
	}
	if(root->rchild)
	{
		print(root->rchild);
	}
}

int main()
{
	int a[maxn];
    int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	cin>>a[i]; 
	node *root;
    root=build(a,n);
    //cout<<root->index<<" "<<root->val<<endl;
    print(root);
    return 0;
}

输出结果:

数组方式

有一个例题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值