数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。(摘自百度百科)
计算机储存,组织数据的方式有很多种,从而导致了数据结构也有很多种类型,从数据的逻辑结构来看,我们可以将其分为线性结构和非线性结构,其主要区别在于各个节点是否只有单一的前驱节点和后继节点
在主要的数据结构中,线性结构包括:线性表栈、队列和串(一维数组);非线性结构包括:广义表(链表,散列表),树,图,堆。
其中比较特殊的是多维数组,多维数组虽然在储存地址上来说是连续的,单其可用矩阵来表示,他们都是两个或多个下标值对应一个元素,是多对一的关系,因此是非线性结构。
在未接触到数据结构前,我们就已经学习过了数组的相关知识,它是一种将同类型的若干变量,按内存地址有序保存的一类集合。
数组类型包括一维数组和多维数组,数组的优点在于,其储存方式简单,可以按照索引(下标)进行查找和遍历,因此应用范围较广。但同时,由于其大小确定后不可更改,添加删除的操作较为复杂,需要移动其他的元素。
由于数组在日常应用中较为频繁,且其数据结构的实现方式较为简单,在此不再赘述。
函数名 | 功能 |
---|---|
c.push_back(elem) | 在尾部插入一个数据 |
c.back() | 传回最后一个数据,不检查这个数据是否存在。 |
c.begin() | 传回迭代器重的可一个数据。 |
c.capacity() | 返回容器中数据个数。 |
c.clear() | 移除容器中所有数据。 |
c.empty() | 判断容器是否为空。 |
c.end() | 指向迭代器中的最后一个数据地址。 |
c.erase(pos) | 删除pos位置的数据,传回下一个数据的位置。 |
c.erase(beg,end) | 删除*[beg,end)*区间的数据,传回下一个数据的位置。 |
c.front() | 传回第一个数据。 |
get_allocator | 使用构造函数返回一个拷贝。 |
c.max_size() | 返回容器中最大数据的数量。 |
c.pop_back() | 删除最后一个数据。 |
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
我们可以用一张图来了解他:
链表的有点在于,灵活,动态的空间占有。
链表的每个节点包括两部分, 一为用户需要用的实际数据,二为下一个结点的地址。 当我们需要查找某个元素时,只要通过从链表的head首地址开始,不断通过节点的next往下寻找,直到找到我们需要的节点即可。
我们可以从某个节点处断开,改变断开处节点左边的next指向一个新节点,再令新节点的next指向右边的节点,从而实现插入操作,同理,我们可以实现删除操作。显而易见,这种操作的实践复杂度位O(1),对比于数组等顺序表,更加灵活。
这里附上链表的一般实现和各类操作(c++版):
#include
using namespace std;
struct List
{
int data; //节点内所存值
List *next; //节点内指向
};
/**
创建节点并返回地址,x为传入值
*/
List *create(int x)
{
List *p=new List;//开辟一个新节点,它会返回一个地址,所以要赋值给指针类型
if(p==NULL)//如果开辟失败或者赋值失败,则要说明
{
cout<<"咋肥四?出错了ε=ε=ε=┏(゜ロ゜;)┛";
return NULL;
}
(*p).data=x; //赋值
(*p).next=NULL; //让节点的指向为空
return p;//返回p的地址
}
/**
添加节点
*/
void addlist(List *pr,int num)//从pr指向的节点开始,添加节点
{
for(int i=1;i<=num;i++)
{
List *p=new List;
(*pr).next=p;
cin>>(*p).data;
(*p).next=NULL;
pr=p;//最后一步是链表得以实现的关键,即将pr指向的节点变更为最新创建的,从而使其再下一次循环中指向下一个节点
}
}
/**
插入节点
*/
void pushlist(List *pr,int index,int index_location)
{
for(int i=1;i<index_location;i++)//从头开始遍历,找到节点。
pr=(*pr).next;
List *p=create(index);
(*p).next=(*pr).next;
(*pr).next=p;
return;
}
/**
删除节点
*/
void delist(List *p,int p_location)
{
for(int i=1;i<p_location-1;i++)
p=(*p).next;
//List *q=p;
(*p).next=(*(*p).next).next;//让目标节点的指向,指向目标节点的下个节点的指向,也就是下一个的下一个
//delete (*q).next;//释放删除节点的空间
}
/**
遍历节点
*/
void displaylist(List *p)
{
while(p!=NULL)
{
cout<<(*p).data<<" ";
p=(*p).next;
}
return;
}
/**
从头遍历节点
并寻找目标值,返回在第几个节点
*/
int findList(List *p,int x)
{
int ans=1;
while(p!=NULL)
{
if((*p).data==x)
return ans;
p=(*p).next;
ans++;
}
return 0;
}
int main()
{
int n,q,q_location;
printf("输入你要存储的节点数\n");
cin>>n;
printf("开始输入值:\n");
cin>>q;
List *head=create(q);//先将第一次创建的节点的地址赋给head,方便遍历
addlist(head,n-1);//加入n-1个节点
cout<<"输入插入的值"<<endl;
cin>>q;
cout<<"插入位置在第几个节点后?"<<endl;
cin>>q_location;
pushlist(head,q,q_location);
cout<<"删除第几个节点?"<<endl;
cin>>q_location;
if(q_location>1)
delist(head,q_location);
else//如果删除第一个节点,则要改变head值
{
head=(*head).next;
}
displaylist(head);//遍历并输出节点
cout<<"寻找目标值:"<<endl;
cin>>q;//输入目标值
cout<<"在第"<<findList(head,q)<<"个节点"<<endl;//输出0表示没有找到
}
#include
//头文件
list<int>li//构造int类型的链表li 这里的数据结构和变量名都可以更改
li.push_back(x)//在尾部插入x
li.pop_back()//尾删除
li.push_front(x)//从头部插入x
li.push_front()//头删除
li.begin()//li的头地址
li.end()//尾地址
li.size()//元素个数
li.empty()//链表是否为空
li.front()//返回头
li.back()//返回尾
li.insert(插入地址,插入值)//在任意地址插入值
li.erase(删除地址)//删除地址
栈又名堆栈,它是一种运算受限的线性表。限定**仅在表尾(栈顶)进行插入和删除操作的线性表。**注意,此时的表尾被称作栈顶,而表头则是栈尾。
向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。所以无论是取出或者插入,都是从栈顶进行的。
用a0作为栈尾,数组头到数组尾为栈顶的生长方向。实现方法简单,不再赘述。
定义栈底指针和栈顶指针,新增的元素指向栈底方向。
具体实现(C++):
#include
using namespace std;
struct Stack
{
int value;
Stack *next;
};
Stack s;
/**
建立栈,并返回元素地址*/
Stack *createStack(int value)
{
Stack *p=new Stack;
(*p).next=NULL;
(*p).value=value;
return p;
}
/**
增加栈,并返回栈顶*/
Stack *addStack(int value,Stack *pr)
{
Stack *p=createStack(value);//创建节点
(*p).next=pr;//让节点,指向旧的栈顶,即上一个节点
return p;
}
/**
删除栈顶元素,并返回栈顶*/
Stack *deleteStack(Stack *pr)
{
Stack *p=(*pr).next;
if(p==NULL)
printf("栈已经空了QAQ\n");
//delete pr;
return p;
}
/**
遍历栈*/
void displayStack(Stack *p)
{
while(p!=NULL)
{
cout<<(*p).value<<" ";
p=(*p).next;//指向栈底的下一个
}
return;
}
int main()
{
Stack *bottom,*top;
int value,n;
printf("请插入栈底元素\n");
cin>>value;
bottom=createStack(value);
top=bottom;
printf("请输入插入栈的个数\n");
cin>>n;
printf("请插入元素:");
for(int i=1;i<=n;i++)
{
cin>>value;
top=addStack(value,top);
}
printf("输入删除个数\n");
cin>>n;
while(n--&&top!=NULL)
top=deleteStack(top);
printf("遍历栈:\n");
displayStack(top);
}
#include //栈
stack<int> s;//参数也是数据类型,这是栈的定义方式
s.empty()//如果栈为空返回true,否则返回false
s.size()//返回栈中元素的个数
s.pop()//删除栈顶元素但不返回其值
s.top()//返回栈顶的元素,但不删除该元素
s.push(X)//在栈顶压入新元素 ,参数X为要压入的元素
队列与栈的操作类似,唯一的不同是队列是先进先出,而栈是先进后出,所以我们采用与栈储存类似的方式,建立对头和队尾,从队头插入,从队尾删除。
建立队头和队尾指针,从队头插入,从队尾删除。其实现和栈类似。
deque<类型>d;//可以用d[i]访问元素。
push_back(x)/push_front(x) //把x压入后/前端
back()/front() //访问(不删除)后/前端元素
pop_back() pop_front() //删除后/前端元素
empty() //判断deque是否空
size() //返回deque的元素数量
clear() //清空deque
支持通过sort(d.begin(),d.end())进行排序。
优先队列通过堆的形式实现,使元素有了优先级
priority_queue<Type, Container, Functional> //其中Type 为数据类型, Container 为保存数据的容器,Functional 为元素比较方式。后两个参数不写默认是vector和operator< 即大根堆。Container 必须是用数组实现的容器,比如 vector, deque 但不能用 list。
//常用操作
priority_queue<int> q;//定义
q.empty() //如果队列为空,则返回true,否则返回false
q.size() //返回队列中元素的个数
q.pop() //删除队首元素,但不返回其值
q.top() //返回具有最高优先级的元素值,但不删除该元素
q.push(item) //在基于优先级的适当位置插入新元素
当需要使用自定义类型和降序时就需要重载 operator< 和自定义cmp
struct A{
int x,y;}a;
struct cmp{//降序
bool operator()(A a,A b)
{
if(a.x==b.x) return a.y>b.y;
return a.x>b.x;
}
};
priority_queue<A,vector<A>,cmp> q;
int main()
{
for(int i=1;i<=6;i++) {cin>>a.x>>a.y;q.push(a);}
while(!q.empty())
{
cout<<q.top().x<<" "<<q.top().y<<endl;
q.pop();
}
}
树结构是一种很重要的非线性数据结构,一个结构要被称作树,它必须满足以下条件:
树的类型有很多,这里我推荐5分钟了解基本的树类型来对树有个初步认识。
以及树的基本术语:
方法一:双亲表示法
利用结构体创建一个结构体数组,结构体中包含data(数据)和parent(父节点下标)
我们通过对这个结构的优化,将结构加入他的第一个孩子节点(fistChild)和每一个节点的下一个兄弟节点(rightSib),从而减小遍历和查找的复杂度。
具体数据结构:
struct Tree
{
int data;
int parent;
int firtChild;
int rightSib;
}t[100000];
方法二:多指针域表示法
创建链表,每一个节点有一个数据和多个指针域,每个指针指向该节点的一个孩子节点
具体数据结构:
struct Tree
{
int data;
int degree;//该节点的度
Tree *node[100];
}t[100000];
(以上的数组都可以用vector代替,从而动态处理空间)
方法三:孩子链表表示法(最优)
为每一个节点创建两个兄弟指针和指向第一个孩子的子指针:
struct Tree
{
int data;
Tree *next;//指向下一个兄弟
Tree *firstChild;//指向第一个孩子
};
二叉树的数据结构实现:
二叉树的定义从5分钟了解基本的树类型这里可以知道,这里只强调一点:二叉树的子树有左右之分,不可以颠倒。
具体实现:
struct Node{
int data;
Node *left;//左孩子
Node *right;//右孩子
};
我们以一道题为例,带入二叉树的实现和遍历方法(先序):新二叉树
AC代码:
/**
1.先序遍历:根->左->右
2.中序遍历:左->根->右
3.后序遍历:左->右->根
*/
#include
using namespace std;
struct Node{
int left=-1;//左孩子
int right=-1;//右孩子
}a[26];
int root=-1;
/**
利用数组建立二叉树
*/
void createTree(int n)
{
char c;
int p;
while(n--)
{
cin>>c;
p='z'-c;
if(root==-1)
root=p;//记录根节点
cin>>c;
if(c!='*')//建左子树
{
a[p].left='z'-c;
}
cin>>c;
if(c!='*')
{
a[p].right='z'-c;
}
}
}
/**
先序遍历二叉树
*/
void displayTree(int p)
{
if(p!=-1)
{
char c='z'-p;
cout<<c;
displayTree(a[p].left);
displayTree(a[p].right);
}
}
int main()
{
int n;
cin>>n;//输入n个节点
createTree(n);//建立树
displayTree(root);//遍历树
}
利用单词只有26个的特性将指针简化成数组并写出代码。重在理解建立和遍历。
我们发现上述建树的情况只是一种特殊的情况,而我们平时最常见的树,一般是二叉搜索树。
他的特点是,每一个根,都有:他的左节点比他小,右节点比他大。
具体实现方法,这里推荐一篇小红书:二叉搜索树(Binary Search Tree)
二叉查找树的查询效率在O(nlogn)到O(N)之间,可以较高效率的实现查找工作
相关例题:二叉搜索树的后序遍历序列
因为不太会写leetcode的c语言程序(跟我学的好像不太一样QWQ),这里附一篇java实现代码(虽然效率较低但可行):
/**
* 树形结构
*/
class Tree{
private Tree left;
private Tree right;
private int data;
public void setLeft(Tree left) {
this.left = left;
}
public void setRight(Tree right) {
this.right = right;
}
public void setData(int data) {
this.data = data;
}
public Tree getLeft() {
return left;
}
public Tree getRight() {
return right;
}
public int getData() {
return data;
}
}
class Solution {
List<Integer> list =new ArrayList();
public boolean verifyPostorder(int[] postorder) {
if(postorder==null)
return false;
else
{
if(postorder.length==0)
return true;
int len=postorder.length-1;
Tree root=new Tree();
root.setData(postorder[len]);
while(len!=0)
{
createTree(postorder[--len],root);
}
listTree(root);
Integer[] listt=new Integer[list.size()];
list.toArray(listt);
if(postorder.length!=listt.length)
return false;
for (int i = 0; i < postorder.length; i++) {
if(postorder[i]!=listt[i])
return false;
}
}
return true;
}
/**
* 后续遍历,生成数组
* @param p
*/
private void listTree(Tree p) {
if(p.getLeft()!=null)
{
listTree(p.getLeft());
}
if(p.getRight()!=null)
{
listTree(p.getRight());
}
list.add(p.getData());
}
/**
* 建立树,添加节点。
* @param i
*/
private void createTree(int i,Tree pr) {
if(i<pr.getData())
{
if(pr.getLeft()==null)
{
Tree p=new Tree();
pr.setLeft(p);
p.setData(i);
}
else
{
createTree(i,pr.getLeft());
}
}
else
{
if(pr.getRight()==null)
{
Tree p=new Tree();
pr.setRight(p);
p.setData(i);
}
else
{
createTree(i,pr.getRight());
}
}
}
}
虽然二叉搜索树可以降低我们对树的索引复杂度,但也只是从O(nlogn)到O(N)的时间复杂度,当二叉树的结构为以下的链树时,其查找复杂度也会达到N(O)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I46JhqA5-1618666688428)(C:\Users\wizard\AppData\Roaming\Typora\typora-user-images\1617697231726.png)]
而平衡二叉树则解决了这个问题。
平衡二叉树( AVL ):
AVL树的特点
具有二叉查找树的特点(左子树任一节点小于父节点,右子树任一节点大于父节点),任何一个节点的左子树与右子树都是平衡二叉树
任一节点的左右子树高度差小于1,即平衡因子为范围为[-1,1] 如上左图根节点平衡因子=1,为AVL树。
AVL树的插入
AVL树插入节点的如下:
根据BST(搜索树)入逻辑将新节点插入树中
从新节点往上遍历检查每个节点的平衡因子,若发现有节点平衡因子不在[-1,1]范围内(即失衡节点u),则通过旋转重新平衡以u为根的子树:
平衡二叉树的删除
平衡二叉树的删除即第一步按照BST规则找到节点,后根据失衡的情况进行旋转。
红黑树(Red-Black Tree,简称R-B Tree),它一种特殊的二叉查找树。
红黑树特点:
(1) 每个节点或者是黑色,或者是红色。
(2) 根节点是黑色。
(3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
(4) 如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
关于它的特性,需要注意的是:
第一,特性(3)中的叶子节点,是指为空(NIL或null)的节点。
第二,特性(5),确保没有一条路径会比其他路径长出两倍。因而,红黑树是相对是接近平衡的二叉树。
红黑树的具体定义和相关操作,我推荐一位大佬的以下的视频:
红黑树快速入门 - 01简介篇,红黑树快速入门 - 02变色与旋转,红黑树快速入门 - 03插入,红黑树快速入门 - 04删除
想要了解其代码实现,则推荐一下这篇博文:红黑树的代码实现详解。
B-tree是一种多路搜索树(并不是二叉的),它有以下定义:
1.定义任意非叶子结点最多只有M个儿子;且M>2;
2.根结点的儿子数为[2, M];
3.除根结点以外的非叶子结点的儿子数为[M/2, M];
4.每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)
5.非叶子结点的关键字个数=指向儿子的指针个数-1;
6.非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
7.非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
8.所有叶子结点位于同一层;
下面是一个M=3的3阶B-tree可以看到非叶节点的节点块最多只有2个值,每个根节点都最多由三个子节点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dAu7wLzA-1618666688430)(C:\Users\wizard\AppData\Roaming\Typora\typora-user-images\1618131680059.png)]
查找过程与平衡二叉树类似,需要注意的是,在查找的时候,对每个节点块的查找可以使用二分。
B-tree的作用降低层次的复杂度,加强查询的稳定性和速度。
B+树是B-树的变体,也是一种多路搜索树:
1.其定义基本与B-树同,除了:
2.非叶子结点的子树指针与关键字个数相同;
3.非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间);
5.为所有叶子结点增加一个链指针;
6.所有关键字都在叶子结点出现;
区间树是在平衡树基础上进行扩展得到的支持以区间为元素的动态集合的操作, 其中最经典的区间树是线段树。
线段树图解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uiLQP5Ns-1618666688432)(C:\Users\wizard\AppData\Roaming\Typora\typora-user-images\1618143457415.png)]
set封装了二叉树结构,是根据元素值进行排序的集合,所插入的元素在集合中唯一,不存在重复元素。
set由红黑树实现,并且对树进行了平衡处理,使得元素在树中分部较为均匀,因此能保持搜索、插入、删除的复杂度在O(logn)。
set用来应对频繁插入删除和查询的数据结构。
set<i>s; //建立set
size() //返回set中的元素数
clear() //清空set
begin() //返回指向set开头的迭代器
end() //返回指向set末尾的迭代器
insert(key) //向set中插入元素key
erase(key) //删除含有key的元素
find(key) //搜索与key一致的元素,并返回指向该元素的迭代器。没有与key一致的元素,则返回末尾end()
利用队列和set的题型:
跳蚱蜢以及这道题的相关题解
求出答案的代码(时间会超限,但因为是填空题,直接copy答案就好了)
#include
using namespace std;
set<string>s;
struct A
{
string s;
int t=1;
};
deque<A>q;
set<string>sets;
int main()
{
A p;
p.s="012345678";
q.push_back(p);
while(!q.empty())
{
int t=q.front().t;
string p1=q.front().s,p2=q.front().s,p3=q.front().s,p4=q.front().s;
int idx=0,i1,i2,i3,i4;
char c;
while(p1[idx]!='0')idx++;
i1=(idx+1)%9,c=p1[i1],p1[i1]='0',p1[idx]=c;
i2=(idx+2)%9,c=p2[i2],p2[i2]='0',p2[idx]=c;
i3=(idx-1+9)%9,c=p3[i3],p3[i3]='0',p3[idx]=c;
i4=(idx-2+9)%9,c=p4[i4],p4[i4]='0',p4[idx]=c;
if(p1.compare("087654321")==0||p2.compare("087654321")==0||p3.compare("087654321")==0||p4.compare("087654321")==0)
{
cout<<t;
break;
}
else
{
t++;
if(sets.find(p1)==sets.end()p.s=p1,p.t=t,q.push_back(p),sets.insert(p1);}
if(sets.find(p2)==sets.end())
{
p.s=p2,p.t=t,q.push_back(p),sets.insert(p2);
}
if(sets.find(p3)==sets.end())
{
p.s=p3,p.t=t,q.push_back(p),sets.insert(p3);
}
if(sets.find(p4)==sets.end())
{
p.s=p4,p.t=t,q.push_back(p),sets.insert(p4);
}
}
q.pop_front();
}
return 0;
}
在计算机科学中,一个图就是一些顶点的集合,这些顶点通过一系列边结对(连接)。顶点用圆圈表示,边就是这些圆圈之间的连线。顶点之间通过边连接。 其中连线没有方向称为无向图,有连线称为有向图。图G(V,E)由一个非空的有限顶点集合 V 和一个有限边集合 E组成
例如,下面便是一个有权值的无向图:
图的概念比较简单, 组织方式比较松散,自由度比较大,但也造成比较高的算法复杂度。所以这里我们侧重于介绍针对图的一些算法。
关于图的遍历,有DFS和BFS两种,网上的资源比较多,我就不赘述了。
核心思想: 找到入度为0的结点,遍历后删掉该结点,直到图中结点为空
具体步骤可参照:拓扑排序详解与实现。
最小生成树是指一个无向图中可以将所以节点遍历到,且边的权值和最小的生成树,对于最小生成树的算法,有K和P两种算法,这里推荐最小生成树(Kruskal(克鲁斯卡尔)和Prim(普里姆))算法动画演示。
一种快速判断一个图是否存在最环的算法,会用到集合的操作。
推荐大佬的视频来了解并查集(Disjoint Set)
这里给出利用并查集写出的图的最小生成树K算法
题目:最小生成树
代码:
#include
using namespace std;
int ans;
const int M=2e5+5,N=5500;
struct A{
int w,x,y;
}ranke[M];//存边
int rankp[N]={1};//存节点深度
int father[N];//存父节点
bool comp(A X,A Y)
{
return X.w<Y.w;
}
int findfather(int p)
{
if(father[p]==-1)
return p;
else
return findfather(father[p]);
}
int uni(int p)
{
int fx=findfather(ranke[p].x);
int fy=findfather(ranke[p].y);
if(fx==fy)
return 0;
if(rankp[fx]>rankp[fy])
father[fy]=fx;
else
if(rankp[fx]<rankp[fy])
father[fx]=fy;
else
{
father[fy]=fx;
rankp[fx]++;
}
return 1;
}
int main()
{
int n,m,w,x,y;
cin>>n>>m;
for(int i=1;i<=n;i++)
father[i]=-1;
for(int i=1;i<=m;i++)
cin>>ranke[i].x>>ranke[i].y>>ranke[i].w;
sort(ranke+1,ranke+m,comp);
for(int i=1;i<=m;i++)
if(uni(i)==1)
ans+=ranke[i].w;
int pan=0;//这里用来判断最后是不是已经将所有节点连成树,方法是判断是否有两个以上的点没有父节点
for(int i=1;i<=n;i++)
if(father[i]==-1)
pan++;
if(pan>1)
cout<<"orz";
else
cout<<ans;
return 0;
}
二分图又称作二部图,是图论中的一种特殊模型。 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。 具体介绍看百科:二分图
我们需要了解以下基本概念和定理:
以下概念可以用来判断是否是二分图
二分图相关算法:待更
可以先看看:二分图及其应用
详见我的另一篇文章:最短路问题
Map是STL的一个关联容器,它提供一对一(其中第一个可以称为关键字,每个关键字只能在map中出现一次,第二个可能称为该关键字的值)的数据 处理能力,由于这个特性,它完成有可能在我们处理一对一数据的时候,在编程上提供快速通道。这里说下map内部数据的组织,map内部自建一颗红黑树(一 种非严格意义上的平衡二叉树),这颗树具有对数据自动排序的功能,所以在map内部所有的数据都是有序的。
//定义:
map<int, string> maps; //定义一个key为int型,value为string型的map
maps[1]="abc";//插入一个key为1,value为"abc"的对,若已经有了该对,则更新
bool empty();// 查询map是否为空
size_t size();// 查询map中键值对的数量
map<int,int>::iterator it;//迭代器
.begin()//begin
.end//end
maps.find(1)//找到key为1的位置,返回迭代器,若查不到值,返回maps.end()
void clear();// 清空map,清空后的size为0
maps.erase(1);//使用关键字直接删除删除成功返回1,没有这个key或删除失败返回0
maps.clear();//全部清除
堆说的清新脱俗一点就是用类似完全二叉树的方式来维护一组数据,增删改查的时间复杂度在O(1)~O(logn)之间,
堆大致分为两类:大根堆和小根堆,简单来说就是根节点是所有数据中最大/小,并且让小的节点在上方。
其最重要的操作就是堆的排序
相关大佬的视频:堆排序
我不是数据结构的生产者,我只是代码的搬运工。。
待更
这里的三大算法,并不是三种算法,而是针对一些利用到数据结构解决的三大类问题进行的总结。
其实现方法利用了二分的思想,每一次选择区间内一值(这里选择区间内mid),把所有大于mid值的放在右边,所有小于mid的放下左边,从而将区间分为两个部分;再二分两个部分实行进一步的排序,最终实现整体的排序(时间复杂度为nlogn)
#include
using namespace std;
const int M=1e5+5;
int a[M];
void qsort(int l,int r)
{
if(l<r)
{
int mid=a[(l+r)/2];
int i=l,j=r;
while(i<=j)
{
while(a[i]<mid)i++;
while(a[j]>mid)j--;
if(i<=j)
{
swap(a[i],a[j]);
i++,j--;
}
}
qsort(l,j);
qsort(i,r);
}
}
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
qsort(1,n);
for(int i=1;i<=n;i++)
cout<<a[i]<<" ";
}