数据结构的重要性
数据结构是程序员的“内功”,根据不同场景的需要,选择合适的数据结构来设计代码。数据结构的定义是:相互之间存在一种或多种特定关系的数据元素的集合。常用的数据结构有线性表、单向链表、双向链表、栈、堆、二叉树、二叉排序树、红黑树、图等。
顺序表(线性表)
-
线性表的顺序存储结构,指的是用一段地址连续的存储单元一次存储线性表的数据元素。
-
优点:
- 无需为表中元素之间的逻辑关系增加额外的储存空间。
- 并且可以快速访问表中任意位置的元素。
-
缺点:
- 插入和删除操作需要移动大量元素。(O(n)平均复杂度)。
- 当线性表长度变化较大时,难以确定存储空间的容量。
- 容易造成存储空间的碎片。
链表
- 链表采用链式存储结构,用一组任意的存储单元存放线性表的元素。
- 优点
- 插入和删除操作更加高效(O(1))
- 不会造成内存碎片
- 不受存储容量的限制。
- 缺点
- 需要额外的存储其他元素的指针
- 不支持随机访问
单向链表的插入、删除节点
在指定位置插入元素
先在链表中找到你要插入位置的节点,将待插入节点的next指向当前节点的next,再将当前节点的next指向待插入节点。
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j;
LinkList p,s;
p=*L;
j=1;
while(p&&j<i)
{
p=p->next;
++j;
}
if(!p||j>i)
return ERROR;
s=(LinkList)malloc(sizeof(Node));
s->data=e;
s->next=p->next;
p->next=s;
return OK;
}
删除指定位置元素
先找到指定位置的节点的前一个节点,前一个节点的next指向待删除节点的next,再释放待删除节点。
Status ListDelete(LinkList *L,int i,ElemType *e)
{
int j;
LinkList p,q;
p=*L;
j=1;
while(p->next&&j<i)
{
p=p->next;
++j;
}
if(!p->next||j>i)
return ERROR;
q=p->next;
p->next=q->next;
free(q);
return OK;
}
双向链表的插入、删除节点
双向链表比单向链表每个节点多了一个前驱的指针,插入删除操作要更复杂一些。
在指定位置插入节点
先找到指定位置的节点,将待插入节点的prior指向当前节点,将待插入节点的next指向当前节点的next,将当前节点的next的prior指向待插入节点,将当前节点的next指向待插入节点。
简单来说就是,先接待插入的前和后,在接待插入两边节点的后和前。
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j;
LinkList p,s;
p=*L;
j=1;
while(p&&j<i)
{
p=p->next;
++j;
}
if(!p||j>i)
return ERROR;
s=(LinkList)malloc(sizeof(Node));
s->data=e;
s->prior=p;
s->next=p->next;
p->next->prior=s;
p->next=s;
return OK;
再指定位置删除节点
先找到制定位置的节点,将待删除节点的prior的next指向待删除节点的next,再将待删除节点的next的prior待删除节点的prior。
Status ListDelete(LinkList *L,int i,ElemType *e)
{
int j;
LinkList p,q;
p=*L;
j=1;
while(p->next&&j<i)
{
p=p->next;
++j;
}
if(!p->next||j>i)
return ERROR;
p->prior->next=p->next;
p->next->prior=p->prior;
free(q);
return OK;
}
栈
栈是限定仅在表尾进行插入和删除操作的线性表。特性是先进后出。
队列
队列是只允许在一端进行插入操作,在另一端进行删除操作的线性表。特性是先进先出。
两个栈实现一个队列
经典题目,不可错过
首先这个组合的队列要有两个栈,另外还要记录最后插入的元素。
插入时就插入到栈A中,顺便记录插入元素。
弹出时,栈B非空的话就从栈B弹出,否则就把A的所有元素压入栈B,再从栈B弹出。
获取头部元素,从栈B获取头部元素,如栈B为空,就把栈A的所有元素都压入栈B,再获取栈B头部元素。
获取尾部元素,直接从记录的最后插入元素获取。
简单来说,插入操作交给栈A,弹出操作交给栈B,栈B为空就把栈A的元素都搬到栈B。
class Queue{
stack<int> stackA;
stack<int> stackB;
int back_elem;
public:
void push(int elem){
stackA.push(elem);
back_elem=elem;
}
void pop(){
if(!stackB.empty()){
stackB.pop();
}else if(!stackA.empty()){
while(!stackA.empty(){
stackB.push(stackA.top());
stackA.pop();
}
stackB.pop();
}else{
//error
}
}
int front(){
if(!stackB.empty())
return stackB.top();
else if(!stackA.empty()){
while(!stackA.empty()){
stackB.push(stackA.top());
satckA.pop();
}
return stackB.top();
}else{
//error
}
}
int back(){
if(!empty())
return back_elem;
else
//error
}
int size(){
return stackA.size()+stackB.size();
}
bool empty(){
return stackA.empty()&&stackB.empty();
}
}
两个队列实现一个栈
同样经典的题目
两个队列的作用是一样的,而且总是 保持其中一个为空,另一个非空。
新push进来的元素总是插入到非空队列中,空队列则用来保存pop操作之后的那些元素,那么此时空队列不为空了,原来的非空队列变为空了,总是这样循环。
class my_stack{
queue<int> queue1;
queue<int> queue2;
public:
void push(int elem){
if(!qeueue1.empty())
queue1.push(elem);
else if(!queue2.empty())
queue2.push(elem);
else
queue1.push(elem);
}
void pop(){
if(queue1.empty()){
while(queue2.size()>1){
queue1.push(queue2.front());
queue2.pop();
}
queue2.pop()
}else{
while(queue1.size()>1){
queue2.push(queue1.front());
queue1.pop();
}
int &data=queue1.front();
queue1.pop();
return data;
}
}
堆
-
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<=K2i+2 ,则称为小堆(或大堆)。
-
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
-
堆中某个节点的值总是不大于或不小于其父节点的值;
-
堆总是一棵完全二叉树。
二叉树
- 二叉树是n(n>=0)个节点的有限集合,由一个根节点和两颗互不相交的左子树和右子树的二叉树组成。
- 每个节点最多有两颗子树,左子树和右子树是有序的
- 满二叉树:所有的分支节点都有左子树和右子树,并且所有的叶子都在同一层上。
- 完全二叉树:如按层序给节点编号,相同编号的节点再完全二叉树和满二叉树中的位置相同。
- 数学性质:
- 第i层最多有2^(i-1)个节点。
- 深度为k的二叉树最多有2^k -1个节点
- 如叶子节点数为n0,度为2的节点为n2,则n0=n2+1
- n个节点的完全二叉树深度为log2 n +1
二叉树的前中后序非递归遍历
- 先序遍历
进入循环,从根节点开始,若当前节点不为空,输出当前节点,当前节点入栈,再进入当前节点的左子树,若当前节点为空,就从栈顶取出节点,进入取出节点的右子树。
//先序遍历二叉树
void PreOrder(BiTree T)
{
stack<BiTree> S;
BiTreeNode* p=T;
while (p|| !S.empty())
{
if (p)
{
printf("%c",p->data);
S.push(p);
p = p->LChild;
}
else
{
p=S.top();
S.pop();
p = p->RChild;
}
}
}
- 中序遍历
与前序遍历一样,只是输出的位置不同,在从栈中取出节点后输出。
//中序遍历二叉树
void InOrder(BiTree T)
{
stack<BiTree T> S;
BiTreeNode* p=T;
while (p || !S.empty())
{
if (p)
{
S.push(p);
p = p->LChild;
}
else
{
p=S.top();
S.pop();
printf("%c", p->data);
p = p->RChild;
}
}
}
- 后序遍历
遍历结点,若结点存在,结点入栈,并对该结点标志位0,表示该结点尚未被遍历其左子树,然后指向其右结点;若结点不存在,满足该结点的左子树已被遍历进入循环,遍历该结点的右子树。不满足退出循环,然后继续出栈得到子结点,并将其压入栈,指向子节点的右孩子,并对该结点标志为1。
//后序遍历
void Postorder(BiTree T)
{
stack<BiTree> S;
BiTreeNode* p=T;
char tag[Maxsize] = {'0'};
while (p || !StackEmpty(*S))
{
if (p)
{
S.push(p);
tag[S->top] = '0';//标志结点是否遍历右子树
p = p->LChild;
}
else
{
while (tag[S->top] == '1') {
p=S.top();
S.pop();
printf("%c",p->data);
}
if (S->top == -1) break;
p=S.top();
S.pop();
S.push(p);
p = p->RChild;
tag[S->top] = '1';
}
}
}
二叉排序树
二叉搜索树虽然可以提高我们查找数据的效率,但如果插入二叉搜索树的数据是有序或接近有序的,此时二叉搜索树会退化为单支树,在单支树当中查找数据相当于在单链表当中查找数据,效率是很低下的。
-
树的左右子树都是AVL树。
-
树的左右子树高度之差(简称平衡因子)的绝对值不超过1。
-
如果一棵二叉搜索树的高度是平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g N ) ,搜索时间复杂度也是 O ( l o g N )
红黑树
虽然AVL数有很强的平衡性,但是当有频繁的插入删除时,会需要大量的旋转保持平衡,使得效率低下。红黑树是一种特殊的二叉查找树,大致平衡,方便插入删除。
红黑树有以下特征
- 节点都是红色或黑色的
- 从根节点到叶子节点,最长的路径不会超过最短路径的2倍。
- 红色节点之间不会相邻
- 根节点是黑色的
二叉排序树与红黑树的区别
1、红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。
2、平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。
哈希表
是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
设计哈希函数
直接定址法、数字分析法、除留余数法、随机数法。
哈希冲突
处理哈希冲突
有两种主要的方法:一个是开放寻址法,一个是拉链法。
- 开放寻址法:当前位置被占用,则寻找下个位置
- 拉链法:当前位置使用链表来存储每个在此位置的元素。
后记
最好让你的实力与你的野心匹配,否则将一事无成。少抱怨,多做事,着眼于当下,不要过于怀旧,也不用过于期待。