您的位置:首页 > 理论基础 > 数据结构算法

数据结构与算法总结3_常用的数据结构(背包,栈和队列)

2016-07-13 15:35 771 查看

0.

这一篇博客介绍背包,栈和队列。

背包是一种不支持从中删除元素的集合数据类型。它的目的是收集元素并遍历所有收集到的元素。用一个更通俗的例子理解背包:有一个非常喜欢收集弹珠的人。他将所有的弹珠都放在一个背包里,一次一个,并且会不时在所有的弹珠中寻找一个具有某种特点的弹珠。因为感觉背包多少有点鸡肋了,在这篇博客中省略背包的具体实现。只是在这里做一个简单的介绍。

队列是一种基于先进先出(FIFO)策略的集合类型。很通俗的一个例子:排队买票的人,先排队的人先买票。任何服务性策略的基本原则都是公平。在提到公平时大部分人的第一个想法就是应该优先服务等待最久的人,这正是先进先出策略的准则。

栈是一种基于后进先出(LIFO)策略的集合类型。一个很通俗的例子:当你的信件在桌上放成一叠时,使用的就是栈,当你有空时,你会一封一封地从上到下阅读它们。这种策略的好处是我们能够及时看到比较新的信件,坏处是如果你不把栈清空,那些较老的信件可能会一直被压在下面。

1. 栈

栈和队列的实现非常相似,只是一个是先进先出一个是后进先出。我们可以通过数组或者链表去实现这两种不同的策略。

现在分别具体看一下这两种数据结构:

Stack(栈)

{

InitStack(); //创建一个空栈

DestoryStack(); //销毁栈

Push(item); //进栈(将item压入栈中)

Pop(); //出栈(将栈中的最后一个元素弹出)

IsEmpty(); //栈是否为空

Size(); //栈中的元素数量

}



这些关于栈的操作中Push()和Pop()是核心。

同样,类似于线性表,栈有两种不同的存储表示方式。一种是顺序存储,另外一种是链式存储。顺序存储就是通过数组去存储栈,链式存储就是通过链表存储栈。

1.栈的顺序存储

栈的顺序存储简称顺序栈,我们在用数组存储栈,同时定义一个top变量来指示栈顶元素在数组中的位置。有了top变量,那数组就可以很容易的模拟出栈进栈操作了。如下图所示,每次进栈和出栈操作都是针对栈顶位置,完全符合栈的先进后出的策略。注意这里的数组通过静态定义(直接定义数组)和动态定义(通过malloc为数组分配内存)都是可以的。



代码如下:

#include<stdio.h>
#define MAXSIZE 20
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];
int top;    //top变量指向栈顶位置,这里的栈顶是指栈中最上面的一个元素所在的位置
}SqStack;

void Push(SqStack *S,ElemType e)
//进栈操作
{
if(S->top==MAXSIZE-1)
{
printf("栈满\n");
return;
}
S->top++;
S->data[S->top]=e;
printf("已进栈\n");
return;
}
void Pop(SqStack *S,ElemType *e)
//出栈操作
{
if(S->top==-1)
//在没有对top是否等于-1之前,不能进行S->data[S->top]操作,因为这样会造成数组溢出。
{
printf("栈为空\n");
return;
}
*e=S->data[S->top];
S->top--;
printf("已出栈\n");
return;
}
int IsEmpty(SqStack S)
{
if(S.top==-1)
{
return 1;
}
return -1;
}
int Size(SqStack S)
{
return S.top+1;
}


2.栈的链式存储

通过链表存储一个栈,和之前类似的定义一个top指针,通过top指针实现后进先出的策略。这里的top指针有两个角色:一个是指向栈顶元素,另一个是链表的头指针。注意这里的进栈操作,是通过前插操作(在链表的前面插入新元素)实现的。



代码实现:

#include<stdio.h>
#include<stdlib.h>
typedef int ElemType;
typedef struct Node
{
ElemType data;
Node *next;
}StackNode, *LinkStackPtr;

typedef struct LinkStack
{
LinkStackPtr top;
int count;
}LinkStack;

void InitStack(LinkStack *S)
{
S->count=0;
S->top=NULL;
}
void Push(LinkStack *S,ElemType e)
//进栈操作
{
LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
if(!s)
exit(-1);
s->next=S->top;
s->data=e;
S->top=s;
S->count++;
printf("进栈完成\n");
return;
}
void Pop(LinkStack *S,ElemType *e)
//出栈操作
{
if(S->count==0)
{
printf("栈已空\n");
return;
}
LinkStackPtr temp=S->top;
S->top=S->top->next;
S->count--;
*e=temp->data;
free(temp);
printf("出栈结束\n");
return;
}
int IsEmpty(LinkStack S)
{
if(S.count==0)
return 1;
return -1;
}
int Size(LinkStack S)
{
return S.count;
}
void main()
//测试
{
LinkStack s;
int e;
InitStack(&s);
Push(&s,1);
Push(&s,2);
Push(&s,3);
printf("%d\n",IsEmpty(s));
printf("%d\n",Size(s));
Pop(&s,&e);
printf("%d\n",e);
Pop(&s,&e);
printf("%d\n",e);
Pop(&s,&e);
printf("%d\n",e);
Pop(&s,&e);
printf("%d\n",IsEmpty(s));
printf("%d\n",Size(s));
}


栈可以通过数组或者链表进行存储,然后通过对数组或者链表进行特定的操作实现后进先出的策略。栈的本质是后进先出的策略,而不是它的具体实现(个人理解)。

2.队列

队列和栈非常的类似,只不过队列采用的是先进先出的策略。类似的,我们同样可以通过数组或者链表去存储队列,然后通过对数组或者链表进行特定的操作实现先进先出的策略。

队列的基本操作:

Queue
{
InitQueue();
EnQueue();    //进队
DeQueue();    //出队
IsEmpty();
Size();
}


关于队列的操作,核心是进队和出对操作。

现在分别看一下队列的两种不同的存储表示方式。

1.队列的顺序存储表示方式

这里稍加思索就会发现一个问题,队列的出队操作是将队列头的元素出队,如果像之前线性表和栈那样去实现队列的话,队列每次出队操作之后都需要将数组的元素前移一位,以保证数组下标为0 的位置不为空。当然可以通过定义两个分别指向队列头和尾的变量去解决这个问题。不过随之而来又会出现一个新的问题,称其为假溢出。当出现下图所示的情况时,实际上队列并没有满,这个问题称其为”假溢出”。



为了解决这些问题,我们将队列的顺序存储表示成循环队列(如下图所示)。



关于循环队列一些更为细节的介绍请参照下面这篇文章。

/article/1305920.html

代码实现:

#include<stdio.h>
#define MAXSIZE 20
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];
int front;
int rear;
}SqQueue;

void InitQueue(SqQueue *Q)
{
Q->front=0;
Q->rear=0;
}
void EnQueue(SqQueue *Q, ElemType e)
//进队
{
if((Q->rear+1)%MAXSIZE==Q->front)
{
printf("队列已满\n");
return;
}
Q->data[Q->rear]=e;
Q->rear=(Q->rear+1)%MAXSIZE;
printf("入队成功\n");
return;
}
void DeQueue(SqQueue *Q,ElemType *e)
//出队
{
if(Q->front==Q->rear)
{
printf("队列为空\n");
return;
}
*e=Q->data[Q->front];
Q->front=(Q->front+1)%MAXSIZE;
printf("出队成功\n");
return;
}
int IsEmpty(SqQueue Q)
{
if(Q.front==Q.rear)
return 1;
return -1;
}
int Size(SqQueue Q)
{
return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;        //循环队列长度计算
}


循环队列中的队列长度的计算以及rear的下一个位置的计算 都是值得仔细琢磨下的,以后还会遇到很多类似的计算。

2.队列的链式存储表示方式

通过带头结点的单链表存储队列。

#include<stdio.h>
#include<stdlib.h>
typedef int ElemType;
typedef struct QNode
{
ElemType data;
QNode *next;
}QNode, *QueuePtr;

typedef struct
{
QueuePtr front; //指向链表的头结点,方便出队操作
QueuePtr rear;  //指向链表的尾部,方便进队操作
}LinkQueue;

void InitQueue(LinkQueue *Q)
{
QueuePtr q=(QueuePtr)malloc(sizeof(QNode));
if(!q)
exit(-1);
q->next=NULL;
Q->front=q;
Q->rear=q;      //初始化front和rear同时指向头结点,代表队列为空
}
void EnQueue(LinkQueue *Q, ElemType e)
//进队
{
QueuePtr q=(QueuePtr)malloc(sizeof(QNode));
if(!q)
exit(-1);
q->data=e;
q->next=Q->rear->next;
Q->rear->next=q;
Q->rear=q;
printf("入队成功\n");
return;
}
void DeQueue(LinkQueue *Q,ElemType *e)
//出队
{
if(Q->front==Q->rear)   //当front等于rear等于头结点时,队列为空
{
printf("队列为空\n");
return;
}
QueuePtr temp=Q->front->next;
*e=temp->data;
Q->front->next=temp->next;
if(temp==Q->rear)
Q->rear=Q->front;
free(temp);
printf("出队成功\n");
return;
}
int IsEmpty(LinkQueue Q)
{
if(Q.front==Q.rear)
return 1;
return -1;
}
int Size(LinkQueue Q)
{
int i=0;
QueuePtr p=Q.front->next;
while(p)
{
p=p->next;
i++;
}
return i;
}
void main()
//测试
{
LinkQueue q;
int e;
InitQueue(&q);
EnQueue(&q,1);
EnQueue(&q,2);
EnQueue(&q,3);
printf("%d\n",Size(q));
printf("%d\n",IsEmpty(q));
DeQueue(&q,&e);
printf("%d\n",e);
DeQueue(&q,&e);
printf("%d\n",e);
DeQueue(&q,&e);
printf("%d\n",e);
DeQueue(&q,&e);
printf("%d\n",Size(q));
printf("%d\n",IsEmpty(q));
}


3.栈的一些应用

(1).左右括号匹配的问题

参见/article/2390515.html

(2).表达式求值

参见 /article/1305930.html

(3).递归和栈

在递归执行一个函数的时候,每一层函数的变量都是保存在栈中,执行到最底层时再依次返回。

下面是原来做的关于递归的笔记。

阶乘 (递归):负数没有阶乘,0的阶乘是1
原理就是:n!=n*(n-1)!

递归,函数每次调用都是一份新的(和原来的没有任何关系),
每次返回的时候都是返回到调用它的位置(逐层回)。

栈:最先分配的最后释放。

递归:
把复杂的大规模的问题化成简单的小规模但是跟原来的类似的问题,
调用同一个函数来处理这个小规模的问题,
在足够简单的时候直接解决问题。

我们使用递归的时候应该考虑两个问题:
(1).如何把规模变小
(2).如何在最简单的时候直接解决问题

(1).汉诺塔问题:
第一步如何把问题简化
先把n个盘子的问题考虑成n-1个和1个的问题
n  :A->C
n-1:A->B
1  :A->C
n-1:B->C
第二步在最简的时候怎么办
最简的时候就是还剩一个盘子的时候,直接移动就可以了。

程序:
void hano(int n,char from,char temp,char to)
{   //n为盘子个数
//from为起始位置,to为目的位置,temp为临时中转

if (n==1)   //在最简的时候如何解决问题
cout<<from<<"->"<<to<<endl;
else        //想办法把问题的规模简化
{
hano(n-1,from,to,temp);
hano(1,from,temp,to);
hano(n-1,temp,from,to);
}
}

如果要输出盘子号的话
程序如下:
void move (int top,int n,char a,char b,char c)
{
//top为最顶的盘子编号,n为盘子个数
// a为起始位置,b为目的位置,c为临时中转

if (n==1)
{
cout <<top<<':'<<a<<"->"<<b<<endl;
}
else
{
move(top,n-1,a,c,b);
move(top+n-1,1,a,b,c);
move (top,n-1,c,b,a);
}
}

(2).把一个整数的各个位输出(利用递归)
void show(int n)
{
if (n<10)       //在最简的时候如何解决问题
cout<<n<<endl;
else            //如何简化问题
{
show(n/10); //注意这两条语句的先后顺序
cout<<n%10<<endl;
}
}


4.栈和队列的常见题目

自己写答案写不好,就贴一些写的比较好的答案的链接。

(1).定义栈的数据结构,要求添加一个min函数,能够得到栈的最小元素。要求函数min、push以及pop的时间复杂度都是O(1)。

/article/5666029.html

(2).两个栈实现一个队列,两个队列实现一个栈

/article/4722593.html

(3).判断push和pop的顺序是否匹配?

这个方法是我下载的一个pdf上面的,我不知道明确的出处。

Peek()函数返回的是栈顶元素的值。



(4).递归翻转栈 和 递归栈排序,空间复杂度O(1)。

这两道题比较难,需要仔细琢磨琢磨。

/article/6001442.html

http://blog.csdn.net/zouhust/article/details/9139867

(5).使用一个数组实现两个栈。





(6).使用一个数组实现三个栈

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: