常见的基本数据结构——栈

栈ADT前端

栈(stack)是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫作栈顶。栈的基本操做有进栈(push)和出栈(pop),前者至关于插入,后者至关于删除最后的元素。在最后插入的元素能够经过使用Top例程在执行Pop以前进行考查。对空栈进行的Pop或Top通常被认为是栈ADT的错误。另外一方面,当运行Push时空间用满是一种实现错误,但不是ADT的错误。算法

栈有时又叫作LIFO(后进先出表)。数组

 

栈的实现函数

因为栈是一个表,所以任何实现表的方法都可以实现栈。两种流行的方法:一种是使用指针实现,一种是使用数组实现。工具

栈的链表实现测试

在表的顶端插入实现Push,在表的顶端删除实现Pop,Top只是返回顶端元素,有时Top和Pop两个也能够合二为一。ui

栈ADT链表的声明实现spa

struct Node;
typedef struct Node *PtrTONode;
typedef PtrToNode Stack;

struct Node{
  ElementType Node;
  PtrToNode Next;
};

测试栈是否为空操作系统

int

IsEmpty(Stack S){

  return S->Next == NULL;

}

建立一个空栈也很简单,咱们只要创建一个头结点,MakeEmpty设置Next指针指向NULL。Push是做为向链表前端进行插入而实现的,其中,表的前端做为栈顶。Top的实现是返回表的前端的元素,Pop是经过删除表的前端元素实现。设计

建立一个空栈的过程

Stack
CreateStack(void){
  Stack S;
  S = malloc(sizeof(struct Node));
  if(S == NULL){
    printf(” out of space”);
  }
  S->Next == NULL;
  MakeEmpty(S);
  return S;
}


void MakeEmpty(Stack S){   if(S == NULL){     Error();   }else{   while(!IsEmpty(S)){     Pop(s);   } }

Push进栈例程

void
Push(ElememtType X, Stack S){
  PtrToNode TemCell;
  TemCell = malloc(sizeof(struct Node));
  if(TemCell == NULL){
    Error();
  }else{
    TemCell->ElementType = X;
    TemCell->Next = S->Next;
    S->Next = TemCell;
  }
}

Pop操做实现

ElementType
Top(Stack S){
  if(!IsEmpty(S))
    return S->Next->Element;
  Error();
  return 0;
}

对于链表的实现,全部的操做基本上都只花费常数的时间,上述的操做出了空栈以外都没有涉及到栈的大小,更没有依赖栈进行循环了。这种实现的缺点是对于malloc和free操做是昂贵的开销。有的缺点能够经过两个栈进行避免,第二个栈初始化为空栈,当单元弹出时,它只是被放入到第二个栈,此后当须要新空间时,首先检查第二个空栈。

栈的数组实现

数组实现避免了指针操做而且是更流行的实现,惟一的不足是它先要声明一个数组的大小。一般栈的实际个数并不会太大,声明一个合理的空间没有什么困难。若是不能的话,那就采用链表实现。数组实现栈是很是简单的,每个栈都有一个TopOfStack,空栈时为-1,当某个元素压入栈时,将TopOfStack加1,而后至Stack[TopOfStack] = X;其中,Stack就是具体栈的数组。出栈时,咱们返回Stack[TopOfStack]的值,而后TopOfStack减1,为了Stack和TopOfStack相对应,它们应该是栈结构的一部分。

上述的操做不只以常数时间运行,并且是以很是快的时间运行。在现代化的计算机中,栈已经成为操做系统指令的一部分。一个影响栈执行效率的问题是错误检查。

 

栈的声明

struct StackRecord;
tepedef struct StructRecord * Stack;

struct StackRecord{
    int Capacity;
    int TopOfStack;
    int ElementType *Array;
}
Stack 
CreateStack(int MaxElement){
  Stack S;
  if(MaxElement < MinStackSize)
  Error();
  S = malloc(sizeof(struct StackRecord));
  if(S == NULL)
    Error();
  S->Array = malloc(sizeof(ElementType) * MaxElement);
  if(S->Array == NULL)
    Error();
  S->Capacity = MaxElements;
  MakeEmpty(S);
  return S;
}

检测栈是否为空

int 
IsEmpty(Stack S){
  return S->TopOfStack == EmptyTOS;
}

建立一个空栈

void 
MakeEmpty(Stack S){
  S->TopOfStack = EmptyTOS;
}

进栈操做

void
Push(ElementType S, Stack S){
  if(IsFull(S))
    Error();
  else
    S->Array[++S->TopOfStack] = X;
}

返回栈顶元素

ElementType
Top(Stack S){
  if(!IsEmpty(S))
  return S->Array[S->TopOfStack];
  Error();
  return 0;
}

从栈顶弹出元素

void
Pop(Stack S){
  if(IsEmpty)
    Error();
  else
    S-TopOfStack—;
}

将Top和Pop进行合并

ElementType
TopAndPop(Stack S){
  if(!IsEmpty(S)){
    return S->Array[S->TopOfStack]; 
  }
  Error();
  return 0;
}

应用

平衡符号

编译器检查你的程序的语法错误,当时经常因为缺乏一个符号形成上百行的错误。在这种状况下,就须要一个工具检验成对出现,每个双符号都要有对应的符号,一个简单的算法就用到栈,以下描述:

作一个空栈。读入字符直到文件尾。若是字符是一个开放字符,则将其推入栈中,若是字符是一个封闭符号,则当栈空时报错。不然,将栈元素弹出,若是弹出的符号不是对应的开放符号,则报错。在文件尾,若是栈非空则报错。

上述的算法是线性的,事实上,它只要对输入进行一趟检验。所以,它是在线的,速度很是的快。

 

后缀表达式

在一个由优先级构成的算术表达式中,咱们一般要根据运算符的有限级进行计算结果。请下面的例子:

4.99 + 5.99 + 6.99 * 1.06 = 18.69

若是没有考虑优先级的话,计算的结果将是19.37.咱们能够经过下面的方法进行计算,操做顺序以下:

4.99 1.06 * 5.99 +6.99 1.06 * +

上面的记发叫作后缀或者逆波兰记法。计算这个问题最容易的办法就是使用一个栈:当碰见数时,就把它放入栈中,在遇到运算符时就做用于栈中弹出的两个数,并将结果推入栈中。

计算一个后缀表达式的时间是线性的O(N),对输入的元素由一些栈操做组成从未花费常数的时间,而且没必要要知道任何的有限顺序。

 

中缀到后缀的转换

栈不只能够计算后缀表达式,并且还能够将一个标准的表达式(中缀表达式)转换成后缀表达式。以下中缀表达式:

a + b * c + (d * e + f) * g

转换成后缀表达式:

a b c * + d e * f + g * +

具体操做是:当读到一个操做数的时候,当即把它放到输出中,操做符不当即输出,保存在某个地方,正确的作法是将遇到的操做符保存在栈中,遇到左括号也放入栈中。

若是碰见一个右括号,那么就将栈元素弹出,将弹出的符号输出直到碰见相匹配的左括号,可是左括号不进行输出。

若是咱们碰见任何其余的符号,那么咱们从栈中弹出栈元素直到发现优先级更低的元素为止。有一个例外,除非是一个)的时候,不然咱们毫不从栈中移除(。对于这种操做,+的优先级最低,(优先级最高。当弹出元素结束后,咱们在将操做符移入栈中。

当到达末尾时,咱们将栈中元素弹出,变成空栈,将符号输出。

一样,这种转换只须要O(N)的,对于运算符时是从左到右的结合的,上面的算法是正确的,否则就须要从新设计。

 

函数调用

当存在函数调用时,须要存储重要的信息,诸如寄存器的值,和返回的地址,都要以抽象的方式存在一张纸上并被置于一个堆的顶部。

递归的不当使用:打印一个链表

void
PrintList(List L){
  if(L != NULL){
    PrintElement(L->Element);
    PrintList(L->Next);
  }
}

这个程序是尾递归,是使用极端不当的例子,尾部涉及在最后一步的递归。

尾递归能够经过将递归调用变成goto语句并在其前加上对函数每一个参数的赋值语句而手工删除。它模拟了递归调用,由于没有什么须要存储的值,在递归调用以后,实际上没有必要知道存储的值。下面是经过goto改造的while循环实现:

void 
PrintList(List L){
  top:
  if(L != NULL){
    PrintElement(L->Element);
    L = L->Next;
    goto top;
  }
}

递归老是可以完全除去,可是有时是至关冗长复杂的。通常方法是使用一个栈来消除,虽然非递归确实比递归程序要快,可是速度的优点代价确实因为去除而使得程序的清晰度不足。

相关文章
相关标签/搜索