算法导论第十章基本数据结构

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-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.3-5

10.4有根树的表示

10.4-1画出下列属性表所示的二叉树,其根节点下标为6.

在画树图过程中,2个下标位置(下标2,下标8)没有用上。具体实现:略。

10.4-2 给定一个n结点的二叉树,写出一个O(n)时间的递归过程,将该树每个结点的关键字输出。

二叉树的递归与非递归实现

10.4-3给定一个n结点的二叉树,写出一个O(n)时间的非递归过程,将该树每个结点的关键字输出。可以使用一个栈作为辅助数据结构。

二叉树的递归与非递归实现

10.4-4  给定一个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);
}
10.4-5给定一个n结点的二叉树,写出一个O(n)时间的非递归过程,将该树每个结点的关键字输出。要求除该树本身的存储空间外只能使用固定量的额外存储空间,且在过程中不得修改该树,即使是暂时的修改也不允许。
以下这段代码,我不太确定是否是O(n)时间,但其他条件都满足。具体看非递归的第三种实现方法。
请看非递归的第三种方法

10.4-6 任意有根树的左孩子右兄弟表示法中每个结点用到三个指针:left-child,right-sibling和parent。对于任何结点,都可以在常数时间到达其父结点,并在与其孩子数呈线性关系的时间内到达所有孩子结点。说明如何在每个结点中只使用两个指针和一个布尔值的情况下,使结点的父结点或者其所有孩子结点可以在与其孩子数呈线性关系的时间内到达
既然少了parent指针,多了布尔值,那么布尔值正好只有ture和false两种值,可以在ture的时候指向其left-child结点,在false的时候指向其right-sibling结点,反过来也可以。

思考题
10.1(链表间的比较)对于下表中的4种链表,所列的每种动态集合操作在最坏情况下的渐近运行时间是多少?    
                                                                      
  未排序的单链表 已排序的单链表 未排序的双向链表 已排序的双向链表
        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)有哨兵

注:successor(L,x)是求指针x的后继,所以至少先查找到x结点的下一个结点,这样就肯定达到了O(n)时间。
       predecessor(L,x)是求指针x的前序,所以至少要查找到x结点的前一个结点,这样就肯定达到了O(n)时间。
       有哨兵的意思就是10.2链表介绍的哑对象,用这种哨兵来求最大最小值。
10.2(利用链表实现可合并堆) 可合并堆支持以下操作:MAKE-HEAP(创建一个空的可合并堆),INSERT,MINUMUM,EXTRACT-MIN和UNION。说明在下列前提下如何用链表实现可合并堆。试着使各操作尽可能高效。分析每个操作按动态集合规模的运行时间。
a.链表是已排序的。
b.链表是未排序的。
c.链表是未排序的,且待合并的动态集合是不相交的。
链表实现可合并堆
10.3(搜索已排序的紧凑链表)练习10.3-4讨论了如何将含n个元素的链表紧凑地维持在数组的前n个位置。假设所有的关键字均不相同,且紧凑链表是已排序的,即对所有的i=1,2,...n且next[i]≠nil,有key[i]<key[next[i]].又假设有一个变量L存储链表的首元素的下标。在这些假设下,试说明可以利用下列随机算法在O(√n)的期望时间内搜索链表。
伪代码如下:
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;
}
   如果忽略过程中第3-7行,就得到一个普通的搜索已排序链表的算法,其中下标i一次指向链表的各个位置,当下标i越出表的末端或key[i]>=k时,搜索终止。在后一种情况中,如果key[i]=k,显然,我们已找到值为k的关键字。但如果key[i]>k,则我们永远也找不到值为k的关键字,因而终止查找时正确的。
   第3-7行意图向前跳至某个随机选择的位置j.当key[j]>key[i]而不大于k时,这种跳跃是有益的。因为这种情况下,j在链表中标识了一个正常搜索中i将要到达的位置。由于链表是紧凑的,所以在1到n中任意选择一个j都会指向链表中的某个对象,而不会是自由表的某个位置。
   我们不直接分析COMPACT-LIST-SEARCH的性能,而是要分析一个相关算法COMPACT-LIST-SEARCH',该算法执行两个独立的循环。该算法增加了一个参数t,用来决定第一个循环迭代次数的上限。
伪代码如下:
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;
}
a.假设COMPACT-LIST-SEARCH'(L,n,k)中第2-8行的while循环经过了t此迭代。论证COMPACT-LIST-SEARCH‘(L,n,k,t)会返回同样的结果,且COMPACT-LIST-SEARCH’中的for循环和while循环的迭代次数之和至少为t.

COMPACT-LIST-SEARCH经过t此循环后,达到循环退出条件key[i]>=k or i=NIL.而COMPACT-LIST-SEARCH‘的while循环的退出条件也是key[i]>=k or i=NIL,所以两个函数经过t次循环满足相同的退出条件,那么对于同一个已排序链表得到的结果也一样相同。其中COMPACT-LIST-SEARCH'的for循环了t次,那么即使while循环一次不执行,那么函数总循环次数也至少为t.

     在COMPACT-LIST-SEARCH'(L,n,k,t)的调用中,设随机变量Xt描述了第2-7行的for循环经t次迭代后链表中从位置i的目标关键字k之间的距离(即通过next指针链)
b.论证COMPACT-LIST-SEARCH'(L,n,k,t)的期望运行时间为O(t+E[Xt]).
for循环的运行时间O(t).由于for循环经过随机跳跃到位置i,然后经过while循环到k,所以while循环运行时间就是i到k的距离,其期望值为O(E[Xt])。那么总的运行时间为O(t+E[Xt]).
算法导论第十章基本数据结构_第1张图片
g:证明COMPACT-LIST-SEARCH的期望运行时间为O(√n).
当仅当t=n/t(t=√n)时,COMPACT-LIST-SEARCH'运行时间达到最小。此时COMPACT-LIST-SEARCH运行时间为O(t)=O(√n)
h.为什么要假设COMPACT-LIST-SEARCH中的所有关键字均不相同?论证当链表中包含重复的关键字时,随机跳跃不一定能降低渐近时间
因为链表中包含重复的关键字时,随机跳跃不一定能降低渐近时间。那么论证为什么不能降低渐近时间,我不太清楚。




你可能感兴趣的:(算法导论第十章基本数据结构)