"Data Structures and Algorithms beauty" <06> stack: how the browser forward and back functions?

Browser forward and backward function, I think you must be very familiar with it?

When you visit a bunch of pages in turn finished abc, page viewed before a b and click your browser's back button, you can view. When you back to the page a, click the forward button, you can view the page again b and c. However, if you back to the page b, click on the new page d, it can no longer pass forward, backward feature to view the page c.

Suppose you are developing engineer Chrome browser, how will you achieve this function?

This use to "stack" such a data structure of our talk today. With this issue, we have to learn the content of today.

 

 

How to understand the "stack"?

On the "stack", I have a very appropriate example, it is a stack of plates stacked together. We usually place when the dishes are put one by one from the bottom up; take the time, we are taken down one by one sequentially from, can not be extracted from the intermediate arbitrary. Last in, first out, who advanced after the out, this is a typical "stack" structure .

From the stack of the operating characteristics of view, the stack is a "limited operation" linear form , insert and delete data only at one end.

The first time I met such a data structure, on the meaning of existence it had a lot of doubts. Because I think, compared to arrays and linked lists, stacks to bring my only limit, and there is no advantage. I directly use an array or list does not like it? Why use this "limited operation" and "stack" mean?

In fact, from a functional point of array or linked list can really replace the stack, but you know, specific data structure is an abstraction of a particular scene , and an array or list user interface exposed too much, really flexible operation free but the use of relatively uncontrolled, naturally more prone to error .

When the characteristics of a data set only involves the insertion and deletion of data at one end, and to meet the LIFO, last-out, we should be preferred "stack" of this data structure.

 

 

How to achieve a "stack"?

From just stack definition, we can see that the stack consists of two main operations, pushing and popping, which is inserted in a data stack and delete data from a top of the stack. Once you understand the definition of the stack, we look at how to implement a stack with code.

In fact, the stack can either use arrays to implement, you can also use the list to achieve. Stack arrays to achieve, we called the order of the stack, with a stack chain to achieve, we call stack chain .

Here implement a stack-based order of the array. Stack-based chain code list to achieve, you can try to write about.

This code is implemented in Java, but does not involve any high-level syntax, and also with the Chinese made a detailed comment, it should be able to read it.

// stack order of the array-based implementation 
public  class ArrayStack {
   Private String [] items;   // array 
  Private  int COUNT;        // stack number of elements 
  Private  int n-;            // stack size 

  // initialize the array, applying a size spatial array of n 
  public ArrayStack ( int n) {
     the this .items = new new String [n];
     the this .n = n;
     the this .count = 0 ; 
  } 

  // stack operation 
  public  Boolean Push (Item String) {
     //Array space is not enough, direct returns false, the stack fails. 
    IF (count == n-) return  to false ;
     // the index item into the position count, and the count by one 
    items [count] = item;
     ++ count;
     return  to true ; 
  } 
  
  // pop operations 
  public String POP () {
     // stack is empty, the process directly returns null 
    IF (count == 0) return  null ;
     // returns the count-1 labeled array elements, and the number of elements in the stack count minus one 
    String tmp = items [ . 1-COUNT ];
     - COUNT;
     return tmp; 
  } 
}

Learn the definition and basic operations, and that it's time to operate, space complexity is how much?

 Whether the order of the stack or chain stack, we only need to store a data array of size n enough. In the process of pushing and popping, only one or two temporary variable storage space, the space complexity is O (1).

Note that this requires storing a data array of size n, not to say the space complexity is O (n). Because the n space is required, it can not be dispensed with. So we say when the space complexity, means in addition to the original data storage space, run the algorithm also require extra storage space.

Space complexity analysis is not very simple? Time complexity is not difficult. Whether the order of the stack or chain stack, stack, stack pop operation involves only the individual data, the time complexity is O (1).

 

 

Support for dynamic expansion of the order of the stack

Just that stack-based arrays to achieve, it is a fixed-size stack, that is to say, you need to specify in advance the stack size when initializing the stack. When the stack is full, you can not go down the stack to add the data. Although the chain stack size is not limited, but you want to store the next pointer, memory consumption is relatively high. That how we can support the dynamic expansion of a stack-based arrays to achieve it?

Do you remember, we are in an array of the section, it is how to achieve a dynamic array support expansion of it? When the array space is not enough, we have to re-apply for a bigger memory, the original copy of all data in the array past. This realization of a dynamic array expansion support.

So, if you want to achieve a dynamic expansion of the stack support, we only need to rely on an array of underlying dynamic expansion of support on it. When the stack is full, we will apply for a larger array, will move the old data to the new array. I draw a diagram, you can understand it shining.

In fact, the order of the stack supports dynamic expansion, we usually are not used to develop. I say this one purpose, mainly want to take you to practice speaking in front of the complexity of the analysis of. Therefore, this section focuses on the complexity of the analysis.

You do not have to memorize the stack, the stack of the complexity of the time, you need to know is the analysis method. To make their own analysis is the real master. Now I'll take you analyze order to support the dynamic expansion of the stack onto the stack, the stack is the time complexity of the operation.

对于出栈操作来说,我们不会涉及内存的重新申请和数据的搬移,所以出栈的时间复杂度仍然是 O(1)。但是,对于入栈操作来说,情况就不一样了。当栈中有空闲空间时,入栈操作的时间复杂度为 O(1)。但当空间不够时,就需要重新申请内存和数据搬移,所以时间复杂度就变成了 O(n)。

为了分析的方便,我们需要事先做一些假设和定义

1. 栈空间不够时,我们重新申请一个是原来大小两倍的数组;

2. 为了简化分析,假设只有入栈操作没有出栈操作;

3. 定义不涉及内存搬移的入栈操作为 simple-push 操作,时间复杂度为 O(1)。

如果当前栈大小为 K,并且已满,当再有新的数据要入栈时,就需要重新申请 2 倍大小的内存,并且做 K 个数据的搬移操作,然后再入栈。但是,接下来的 K-1 次入栈操作,我们都不需要再重新申请内存和搬移数据,所以这 K-1 次入栈操作都只需要一个 simple-push 操作就可以完成。为了让你更加直观地理解这个过程,我画了一张图。

你应该可以看出来,这 K 次入栈操作,总共涉及了 K 个数据的搬移,以及 K 次 simple-push 操作。将 K 个数据搬移均摊到 K 次入栈操作,那每个入栈操作只需要一个数据搬移和一个 simple-push 操作。以此类推,入栈操作的均摊时间复杂度就为 O(1)。

通过这个例子的实战分析,也印证了前面讲到的,均摊时间复杂度一般都等于最好情况时间复杂度。因为在大部分情况下,入栈操作的时间复杂度 O 都是 O(1),只有在个别时刻才会退化为 O(n),所以把耗时多的入栈操作的时间均摊到其他入栈操作上,平均情况下的耗时就接近 O(1)。

 

 

栈在函数调用中的应用

前面我讲的都比较偏理论,我们现在来看下,栈在软件工程中的实际应用。栈作为一个比较基础的数据结构,应用场景还是蛮多的。其中,比较经典的一个应用场景就是函数调用栈

我们知道,操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。为了让你更好地理解,我们一块来看下这段代码的执行过程。

int main() {
   int a = 1; 
   int ret = 0;
   int res = 0;
   ret = add(3, 5);
   res = a + ret;
   printf("%d", res);
   reuturn 0;
}

int add(int x, int y) {
   int sum = 0;
   sum = x + y;
   return sum;
}

从代码中我们可以看出,main() 函数调用了 add() 函数,获取计算结果,并且与临时变量 a 相加,最后打印 res 的值。为了让你清晰地看到这个过程对应的函数栈里出栈、入栈的操作,我画了一张图。图中显示的是,在执行到 add() 函数时,函数调用栈的情况。

 

 

栈在表达式求值中的应用

我们再来看栈的另一个常见的应用场景,编译器如何利用栈来实现表达式求值

为了方便解释,我将算术表达式简化为只包含加减乘除四则运算,比如:34+13*9+44-12/3。对于这个四则运算,我们人脑可以很快求解出答案,但是对于计算机来说,理解这个表达式本身就是个挺难的事儿。如果换作你,让你来实现这样一个表达式求值的功能,你会怎么做呢?

实际上,编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。

如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。

我将 3+5*8-6 这个表达式的计算过程画成了一张图,你可以结合图来理解我刚讲的计算过程。

 

这样用两个栈来解决的思路是不是非常巧妙?你有没有想到呢?

 

 

栈在括号匹配中的应用

除了用栈来实现表达式求值,我们还可以借助栈来检查表达式中的括号是否匹配。

我们同样简化一下背景。我们假设表达式中只包含三种括号,圆括号 ()、方括号 [] 和花括号{},并且它们可以任意嵌套。比如,{[] ()[{}]}或 [{()}([])] 等都为合法格式,而{[}()] 或 [({)] 为不合法的格式。那我现在给你一个包含三种括号的表达式字符串,如何检查它是否合法呢?

这里也可以用栈来解决。我们用栈来保存未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号。如果能够匹配,比如“(”跟“)”匹配,“[”跟“]”匹配,“{”跟“}”匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。

当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明有未匹配的左括号,为非法格式。

 

 

解答开篇

好了,我想现在你已经完全理解了栈的概念。我们再回来看看开篇的思考题,如何实现浏览器的前进、后退功能?其实,用两个栈就可以非常完美地解决这个问题。

我们使用两个栈,X 和 Y,我们把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。当我们点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。

比如你顺序查看了 a,b,c 三个页面,我们就依次把 a,b,c 压入栈,这个时候,两个栈的数据就是这个样子:

当你通过浏览器的后退按钮,从页面 c 后退到页面 a 之后,我们就依次把 c 和 b 从栈 X 中弹出,并且依次放入到栈 Y。这个时候,两个栈的数据就是这个样子:

这个时候你又想看页面 b,于是你又点击前进按钮回到 b 页面,我们就把 b 再从栈 Y 中出栈,放入栈 X 中。此时两个栈的数据是这个样子:

这个时候,你通过页面 b 又跳转到新的页面 d 了,页面 c 就无法再通过前进、后退按钮重复查看了,所以需要清空栈 Y。此时两个栈的数据这个样子:

 

 

内容小结

我们来回顾一下今天讲的内容。栈是一种操作受限的数据结构,只支持入栈和出栈操作。后进先出是它最大的特点。栈既可以通过数组实现,也可以通过链表来实现。不管基于数组还是链表,入栈、出栈的时间复杂度都为 O(1)。除此之外,我们还讲了一种支持动态扩容的顺序栈,你需要重点掌握它的均摊时间复杂度分析方法。

 

资源地址: 点此进入

 

 

 

 

 


注: 本文出自极客时间(数据结构与算法之美),请大家多多支持王争老师。如有侵权,请及时告知。

 

Guess you like

Origin www.cnblogs.com/zzd0916/p/11981475.html