10.1栈与队列
(1) 栈
概念定义:栈属于动态集合,采用先进后出策略(LIFO)。基本操作是压入(PUSH)和弹出(POP)。如果s.top=0,表示栈空,如果试图对空栈进行POP操作会发生下溢(underflow)。如果s.top>n,表示栈满,如果进行PUSH操作会发生上溢(overflow)。
基本代码:
//基本的栈操作
#include <iostream>
#include <time.h>
using namespace std;
const n=10;
struct stack
{
int top;
int Array[n];
}s;
//初始化一个空栈
int Init_Stack()
{
s.top = 0;//top为0表示空栈
return 1;
}
//判断栈是否为空
bool stack_empty(int Array[])
{
if (s.top==0)
{
return true;
}
else
{
return false;
}
}
//入栈
void stack_push(int Array[],int x)
{
if (s.top==n)
{
cerr<<"overflow!"<<endl;
}
else
{
s.top++;
s.Array[s.top]=x;
}
}
//出栈
int stack_pop(int Array[])
{
if (stack_empty(s.Array))
{
cerr<<"underflow"<<endl;
return -1;
}
else
{
s.top--;
return s.Array[s.top+1];
}
}
void main()
{
struct stack s;
Init_Stack();
int x=0;
srand( (unsigned)time( NULL ) );
for ( int i=0;i<n;i++)
{
x=rand()%100;
stack_push(s.Array,x);
}
for (i=0;i<n;i++)
{
cout<<stack_pop(s.Array)<<endl;
}
}
(2) 队列
概念定义:队列属于动态集合,采用先进先出策略(FIFO)。基本操作是出队(enqueue)和出队(dequeue)。如果head=tail,表示队列为空,如果试图对空队列进行enqueue操作会发生下溢(underflow)。如果head=tail+1,表示队列满,如果进行dequeue操作会发生上溢(overflow)。
基本代码:
//基本的队列操作
/*#include <iostream>
#include <time.h>
using namespace std;
const n=5;
struct Queue
{
int head;
int tail;
int Array[n];
}Q;
void Init_Queue()
{
Q.head=Q.tail=1;//表示队列为空栈
}
//判断栈是否为空
bool Queue_empty(int Array[])
{
if (Q.head==Q.tail)
{
return true;
}
else
{
return false;
}
}
void ENQueue(int Array[],int x)
{
if (Q.head==Q.tail+1)
{
cerr<<"overflow!"<<endl;
}
else
{
Q.Array[Q.tail]=x;
if (Q.tail==n)
{
Q.tail=0;
}
else
{
Q.tail++;
}
}
}
int DEQueue(int Array[])
{
int x=0;
if (Queue_empty(Q.Array))
{
cerr<<"underflow"<<endl;
}
else
{
x=Q.Array[Q.head];
if (Q.head==n)
{
Q.head=0;
}
else
{
Q.head++;
}
}
return x;
}
void main()
{
struct Queue Q;
Init_Queue();
int x=0;
srand( (unsigned)time( NULL ) );
for ( int i=0;i<n;i++)
{
x=rand()%100;
ENQueue(Q.Array,x);
}
for (i=0;i<n;i++)
{
cout<<DEQueue(Q.Array)<<endl;
}
}
10.1-1 仿照图10-1,说明对一个存储在数组S[1..6]中的,初始为空的栈S,依次执行PUSH(S,4),PUSH(S,1),PUSH(S,3),POP(S),PUSH(S,8)以及POP(S)操作后的结果。
操作结果4,1
10.1-2 说明如何用一个数组A[1..n]来实现两个栈,使得两个栈中的元素总数不到n时,两者都不会发生上溢。注意PUSH和POP操作的时间应为O(1)
//一个数组实现两个栈10.1-3仿照图10-2,说明对一个存储在数组Q[1..6]中的,初始为空的队列Q,依次执行ENQUEUE(Q,4),ENQUEUE(Q,1),ENQUEUE(Q,3),DEQUEUE(Q),ENQUEUE(Q,8)以及DEQUEUE(Q)操作后的结果。
#include <iostream>
#include <time.h>
using namespace std;
const n=10;
struct stack
{
int top1;
int top2;
int Array[n];
}s;
//初始化一个空栈
void Init_Stack()
{
s.top1 = -1;//top1为0表示以左端端点为初始栈顶的栈(简称左栈)空栈
s.top2=n;//top2为n-1表示以右端端点为初始栈顶的栈(简称右栈)空栈
}
//判断栈是否为空
bool stack_empty(int Array[],int flag)
{
if (s.top1<-1&&flag==0)
{
return true;
}
if (s.top2>n&&flag==1)
{
return true;
}
return false;
}
//入栈
void stack_push(int Array[],int x,int flag)
{
if (flag==0)
{
s.top1++;
if (s.top1>=s.top2)
{
cerr<<"试图入左栈,发生上溢"<<endl;
}
else
{
s.Array[s.top1]=x;
}
}
else
{
s.top2--;
if (s.top1>=s.top2)
{
cerr<<"试图入右栈,发生上溢!"<<endl;
}
else
{
s.Array[s.top2]=x;
}
}
}
//出栈
int stack_pop(int Array[],int flag)
{
if (flag==0)
{
s.top1--;
if (stack_empty(s.Array,flag))
{
cerr<<"试图出左栈,发生下溢!"<<endl;
return -1;
}
else
{
return s.Array[s.top1+1];
}
}
else
{
s.top2++;
if (stack_empty(s.Array,flag))
{
cerr<<"试图出右栈,发生下溢"<<endl;
return -1;
}
else
{
return s.Array[s.top2-1];
}
}
}
bool All_data_out_of_stack()//检测是否两个栈都为空,目的是使数组中所有数据都能输出
{
if (s.top1<-1&&s.top2>n)
{
cout<<"数组全部数据已全部出栈!"<<endl;
return true;
}
else
return false;
}
void main()
{
struct stack s;
Init_Stack();
int x=0,flag=0;
static h1=0,h2=0;
srand( (unsigned)time( NULL ) );
cout<<"随机对两个栈进行入栈操作中。。"<<endl;
for ( int i=0;i<n;i++)
{
x=rand()%100;
flag=rand()%(1-0+1)+0;
stack_push(s.Array,x,flag);
}
cout<<"随机对两个栈进行出栈操作中。。"<<endl;
for (i=0;;i++)
{
flag=rand()%(1-0+1)+0;
int t=stack_pop(s.Array,flag);
if (All_data_out_of_stack())
{
break;
}
else if(t!=-1)
{
cout<<t<<endl;
}
}
}
操作结果3,8
10.1-4 重写ENQUEUE和DEQUEUE,使之能处理队列的下溢和上溢。
最上面已经给出相关代码。
10.1-5 栈的插入和删除操作都是在一端进行的,而队列的插入和删除确实在两头进行的。另有一种双端队列,其两端都可以做插入和删除操作。对于一个用数组构造的双端队列,请写出四个在两端进行插入和删除操作的过程。要求运行时间都为O(1)。
//双端队列
#include <stdio.h>
#include <iostream>
#include <time.h>
using namespace std;
#define MAX 10
struct queue
{
int Array[MAX];
int head;
int tail;
}Q;
void Init_Queue()
{
Q.head=Q.tail=0;//表示队列为空栈
}
int queue_empty()
{
return Q.head == Q.tail;
}
//队头插入
int ENqueue_head(int Array[],int x)
{
if( Q.head==-1 )
{
cout<<"对头插入失败!"<<endl;
return -1;
}
else
{
Q.Array[Q.head--] = x;
return Q.head;
}
}
//队尾插入
int ENqueue_tail(int Array[], int x )
{
if(Q.tail==MAX)
{
cout<<"队尾插入失败!"<<endl;
return -1;
}
else
{
Q.Array[Q.tail++] = x;
return Q.tail;
}
}
int DEqueue_head()
{
if( Q.head>Q.tail )
{
cout<<"对头已经到队列末端,不能删除!"<<endl;
return -1;
}
else
{
return Q.Array[Q.head++];
}
}
int DEqueue_tail()
{
if( Q.head>Q.tail )
{
cout<<"队尾已经到队列末端,不能删除!"<<endl;
return -1;
}
else
{
return Q.Array[Q.tail--];
}
}
10.1-6说明如何用两个栈来实现一个队列,并分析相关队列操作的运行时间。
10.1-7说明如何用两个队列来实现一个栈,并分析有关栈操作的运行时间。
两个栈实现一个队列与两个队列实现一个栈
10.2链表
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序存储结构,操作复杂。
书中给出的伪代码翻译成C/C++代码:
//书中双向链表无哨兵实现
#include <iostream>
#include <time.h>
using namespace std;
#define LEN sizeof(struct List)
struct List
{
struct List*prev;
struct List*next;
struct List*head;
int key;
};
//查找数据
struct List*search(struct List L,int k)
{
struct List*x;
x=L.head;
while (x!=NULL&&x->key!=k)
{
x=x->next;
}
return x;
}
//插入数据
struct List*insert(struct List L,struct List*x)
{
x->next=L.head;
if (L.head!=NULL)
{
L.head->prev=x;
}
L.head=x;
x->prev=NULL;
return L.head;
}
//删除数据
struct List*Delete(struct List L,struct List*x)
{
if (x->prev!=NULL)
{
x->prev->next=x->next;
}
else
{
L.head=x->next;
}
if (x->next!=NULL)
{
x->next->prev=x->prev;
}
return L.head;
}
//打印数据
void Print(List L)
{
struct List*p = L.head;
while(p)
{
cout<<p->key<<' ';
p = p->next;
}
cout<<endl;
}
void main()
{
struct List L,*x,*t;
srand( (unsigned)time( NULL ) );
L.head=NULL;x=NULL;
for (int i=0;i<10;i++)
{
x= (struct List*)malloc(LEN);
x->key=rand()%100;
if (i==6)
{
t=x;
}
L.head=insert(L,x);
}
Print(L);
x=search(L,60);
if (x==NULL)
{
cout<<"未找到数据!"<<endl;
}
else
{
cout<<"已经找到数据"<<x->key<<endl;
}
L.head=Delete(L,t);
Print(L);
}
//书中双向链表有哨兵实现/*#include <iostream>#include <time.h>using namespace std;#define LEN sizeof(struct List)struct List{ struct List*prev; struct List*next; struct List*nil; int key;};struct List*Initialization(struct List &L){ L.nil; L.nil=(struct List*)malloc(LEN); L.nil->next=L.nil; L.nil->prev=L.nil; return L.nil;}struct List*search(struct List &L,int k){ struct List*x; x=L.nil->next; while (x!=L.nil&&x->key!=k) { x=x->next; } return x;}struct List*insert(struct List &L,struct List*x){ x->next=L.nil->next; L.nil->next->prev=x; L.nil->next=x; x->prev=L.nil; return L.nil;}struct List*Delete(struct List &L,struct List*x){ x->prev->next=x->next; x->next->prev=x->prev; return L.nil;}//打印数据void Print(List L) { struct List*p = L.nil->next; if(p!=L.nil) { while(p!=L.nil) { cout<<p->key<<' '; p = p->next; } cout<<endl; } else { cout<<"此链表为空!"<<endl; }} void main(){ struct List L,*x,*t; Initialization(L); Print(L); srand( (unsigned)time( NULL ) ); x=NULL; for (int i=0;i<10;i++) { x= (struct List*)malloc(LEN); x->key=rand()%100; if (i==6) { t=x; } insert(L,x); } Print(L); x=search(L,60); if (x==L.nil) { cout<<"未找到数据!"<<endl; } else { cout<<"已经找到数据"<<x->key<<endl; } Delete(L,t); Print(L);}10.2-1动态集合上的操作INSERT能否用一个单链表在O(1)时间内实现?对DELETE操作呢?
插入操作:可以。
删除操作:如果需要删除指定的数据,那么需要查询操作在O(n)时间内找出这个值,然后再删除。如果按顺序删除,则只需O(1)时间,所以最坏情况下是O(n).
10.2-2用一单链表L实现一个栈,要求PUSH和POP操作的时间仍为O(1).
入栈操作相当于插入操作。出栈相当于删除操作。代码如下:
10.2-3用一单链表L实现一个队列,要求ENQUEUE和DEQUEUE操作的时间仍为O(1).
同上,入队操作相当于插入操作。出队相当于删除操作。代码如下:
10.2-4如前文所写,LIST-SEARCH‘过程的每一次循环迭代都需要做两个测试:一个检查x≠nil[L],一个检查key[x]≠k.说明如何能够在每次迭代中,省去对x≠nil[L]的检查。
struct List*search(struct List &L,int k)
{
struct List*x;
x=L.nil->next;
L.nil->key=k;//开始先把待查找的值赋值给哨兵。
while (x->key!=k)//直到x->key等于待查找的值才结束。
{
x=x->next;
}
if (x==L.nil)//如果x等于哨兵,说明链表里没有此数据
{
L.nil->key=NULL;//然后恢复哨兵原来的值
return L.nil;
}
else//否则说明链表里含有这个数据。
{
return x;
}
}//这样while循环就不用每次都检测x是否等于哨兵这个判断,只需要在循环结束后,做一次类似判断即可。
10.2-5 用环形单链表来实现字典操作INSERT,DELETE和SEARCH,并给出它们的运行时间
10.2-6 动态集合操作UNION以两个不想交的集合S1和S2作为输入,输出集合S=S1∪S2包含了S1和S2的所有元素。该操作常常会破坏S1和S2。说明应如何选用一种合适的表数据结构,以便支持在O(1)时间内的UNION操作。
//求两个链表的并集
#include <iostream>
#include <time.h>
using namespace std;
#define LEN sizeof(struct List)
struct List
{
int key;
struct List*next;
struct List*head;
};
//插入数据
struct List*insert(struct List *&p,struct List *head,struct List*x)
{//由于链表没有像数组一样的越界限制,所以不用检查上溢。
x=(struct List*)malloc(LEN);//最多会出现不能申请堆中内存。
x->key=rand()%100;
if (head==NULL)
{
p=head=x;
}
else
{
p->next=x;
p=p->next;
}
p->next=NULL;
return head;//p遍历到链表尾部
}
struct List*UNION(struct List *head1,struct List *head2,struct List *tail1,struct List *tail2)
{//只要头指针不为NULL,那么它的尾指针一定不为空,所以我们只检测头指针是否为NULL。其实就是两个链表拼接起来。
if (head1!=NULL)
{
if (head2!=NULL)
{
tail2->next=head1;//或者tail1->next=head2
return head2;//return head1
}
else if(head2==NULL)
{
return head1;
}
}
else
{
if (head2!=NULL)
{
return head2;
}
else if(head2==NULL)
{
return NULL;
}
}
}
void Print(struct List *head)
{
struct List*p =head;
if(p!=NULL)
{
while(p)
{
cout<<p->key<<"->";
p = p->next;
}
}
else
{
cout<<"此链表为空!"<<endl;
}
}
void main()
{
struct List S1,S2;
S1.head=NULL,S2.head=NULL;
struct List *x=NULL,*p1=NULL,*p2=NULL;
srand( (unsigned)time( NULL ) );
cout<<"数据插入链表中。。"<<endl;
for (int i=0;i<10;i++)
{
S1.head=insert(p1,S1.head,x);
}
Print(S1.head);
cout<<endl;
cout<<"数据插入链表中。。"<<endl;
for ( i=0;i<10;i++)
{
S2.head=insert(p2,S2.head,x);
}
Print(S2.head);
cout<<endl;
UNION(S1.head,S2.head,p1,p2);
Print(UNION(S1.head,S2.head,p1,p2));
}
10.2-7 请给出一个Θ(n)时间的非递归过程,它对含n个元素的单链表的链进行逆转。除了链表本身占用的空间外,该过程应仅使用固定量的存储空间。
网上找到的两种逆转过程,感觉函数1没函数2好。
//反转链表函数110.2-8 不太懂。看了很多答案也没搞清楚是怎么回事?
struct List*Reverse_list(struct List *p)
{
struct List *q,*r,*t;
t=(struct List*)malloc(LEN);
t->key=0x7fffffff;
t->next=head;
head=t;
p=head;
q=p->next;
while (q!=NULL)
{
r=q->next;
q->next=p;
p=q;
q=r;
}
return p;
}
//反转链表函数2
void Reverse()
{
struct List*p = NULL, *q = head, *r;
//依次修改指针,让q是p->next,令q->next=p
while(1)
{
r = q->next;
q->next = p;
if(r == NULL)
{
head = q;
break;
}
p = q;
q = r;
}
}
10.3指针和对象的实现
当有些语言不支持指针和对象数据类型时,我们可以用数组和数组下标构造对象和指针。这种链表称为静态链表。
对象的多数组和单数组表示基本的字典操作+申请分配内存
10.3-1 请画出序列<13,4,8,19,5,11>存储在以多重数组表示的爽链表中的形式。另画出在单数组表示下的形式。多重数组表示法:数组下标位置: 0, 1, 2, 3, 4, 5
next数组: 2, 4, 5, 1, 0, -1
key数组: 19, 4, 5,13, 8,11
prev数组: 4, 3, 0, -1, 1, 2
链表顺序为: 13->4->8->19->5->11 head=3
单数组表示法: 数组下标位置: 0, 1, 2, 3, 4, 5, 6, 7,8, 9,10,11,12,13,14,15,16,17
对于下标i来说 key[i],next[i+1],prev[i+2] 19, 6,12,4,12,9,5,15,0,13, 3,-1, 8, 0, 3, 11,-1, 6
链表顺序为: 13->4->8->19->5->11 head=9
10.3-2 对一组用单数组表示的同构对象,写出其过程ALLOCATE-OBJECT和FREE-OBJECT。
10.3-3 在ALLOCATE-OBJECT和FREE-OBJEC过程的实现中,为什么不需要设置重置对象的prev属性呢?
next属性决定了prev属性,所以设置完next属性,prev属性也在程序运行中被设置好了。
10.3-4 我们往往希望双向链表的所有元素在存储器中保持紧凑,例如,在多数组表示中占用前m个下标位置.(在页式虚拟存储的计算环境下,即为这种情况。)假设除指向链表本身的指针外没有其他指针指向该链表元素,试说明如何实现过程ALLOCATE-OBJECT和FREE-OBJECT,使得该表示保持紧凑。(提示,使用栈的数组实现)。
10.3-5 设L是一个长度为n的双向链表,存储于长度为m的数组key,prev和next中。假设这些数组由维护双链*表F的两个过程ALLOCATE-OBJECT和FREE-OBJECT进行管理。又假设m个元素中,恰有n个元素在链表L上,m-n个在*表上。给定链表L和*表F,试写出一个过程COMPACTIFY-LIST(L,F),用来移动L的元素使其占用数组中1,2,。。。n的位置,调整*表F以保持其正确性,并且占用数组中n+1,n+2....,m的位置。要求缩写的过程运行时间应为θ(n),且只使用固定量的额外存储空间。请证明所写的过程是正确的。
10.4有根树的表示
10.4-1画出下列属性表所示的二叉树,其根节点下标为6.
在画树图过程中,2个下标位置(下标2,下标8)没有用上。具体实现:略。
10.4-2 给定一个n结点的二叉树,写出一个O(n)时间的递归过程,将该树每个结点的关键字输出。
10.4-3给定一个n结点的二叉树,写出一个O(n)时间的非递归过程,将该树每个结点的关键字输出。可以使用一个栈作为辅助数据结构。
//左孩子右兄弟表示多叉树
#include <iostream>
using namespace std;
#define LEN sizeof(struct Tree)
struct Tree*root=NULL;
struct Tree
{
int key;
struct Tree*left_child;
struct Tree*right_sibling;
};
struct Tree*create(struct Tree**p)
{//注意创建多叉树的过程中,根结点没有右兄弟,所以最后回溯到根结点时,要把根节点的右兄弟设置为0.
*p=new struct Tree[LEN];
cin>>(*p)->key;
if ((*p)->key!=0)
{
create(&(*p)->left_child);
create(&(*p)->right_sibling);
}
return root;
}
//先序遍历递归过程
void PreOderTraverse(struct Tree *p)
{
if (p->key)
{
cout<<p->key<<" ";
PreOderTraverse(p->left_child);
PreOderTraverse(p->right_sibling);
}
}
void main()
{
struct Tree *p=NULL;
create(&p);
PreOderTraverse(p);
}
未排序的单链表 | 已排序的单链表 | 未排序的双向链表 | 已排序的双向链表 | |
SEARC(L,k) | O(n) | O(n) | O(n) | O(n) |
INSERT(L,x) | O(1) | O(n) | O(1) | O(n) |
DELETE(L,x) | O(n) | O(n) | O(n) | O(n) |
SUCCESSO(L,x) | O(n) | O(n) | O(n) | O(n) |
PREDECESSO(L,x) | O(n) | O(n) | O(n) | O(n) |
MINIMUM(L) | O(n) | O(1)有哨兵有尾指针 | O(n) | O(1)有哨兵 |
MAXIMUM(L) | O(n) | O(1)有哨兵有尾指针 | O(n) | O(1)有哨兵 |
COMPACT_LIST_SEARCH(L,n,k)
{
i=L;
while i!=NILand key[i]<k
{
j=RANDOM(1,n);
if (key[i]<key[j]and key[j]<=k)
{
i=j;
if (key[i]==k)
{
return i;
}
}
i=next[i];
}
if (i==NIL or key[i]>k)
{
return NIL;
}
else return i;
}
COMPACT_LIST_SEARCH'(L,n,k,t)
{
i=L;
for q=1 to t
{
j=RANDOM(1,n);
if key[i]<key[j] and key[j]<=k
{
i=j;
if key[i]==k
{
return i;
}
}
}
while i!=NIl and key[i]<=k
i=next[i];
if i==NIL or key[i]>k
return NIL;
else return i;
}