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)
//一个数组实现两个栈 #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; } } }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)操作后的结果。
操作结果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好。
//反转链表函数1 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.2-8 不太懂。看了很多答案也没搞清楚是怎么回事?
10.3指针和对象的实现
当有些语言不支持指针和对象数据类型时,我们可以用数组和数组下标构造对象和指针。这种链表称为静态链表。
对象的多数组和单数组表示基本的字典操作+申请分配内存
对象的多数组和单数组表示
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.3-5
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; }