转载请注明出处:http://www.cnblogs.com/wangyingli/p/5931782.htmlhtml
上一篇《数据结构与算法(二),线性表》中介绍了数据结构中线性表的两种不一样实现——顺序表与链表。这一篇主要介绍线性表中比较特殊的两种数据结构——栈与队列。首先必须明确一点,栈和队列都是线性表,它们中的元素都具备线性关系,即前驱后继关系。java
栈(也称下压栈,堆栈)是仅容许在表尾进行插入和删除操做的线性表。咱们把容许插入和删除的一端称为栈顶(top),另外一端称为栈底(bottom)。栈是一种后进先出(Last In First Out)的线性表,简称(LIFO)结构。栈的一个典型应用是在集合中保存元素的同时颠倒元素的相对顺序。node
抽象数据类型:算法
栈同线性表同样,通常包括插入、删除等基本操做。其基于泛型的API接口代码以下:数组
public interface Stack<E> { //栈是否为空 boolean isEmpty(); //栈的大小 int size(); //入栈 void push(E element); //出栈 E pop(); //返回栈顶元素 E peek(); }
栈的实现一般有两种方式:数据结构
栈的顺序存储结构实际上是线性表顺序存储结构的简化,咱们能够简称它为「顺序栈」。其存储结构以下图:ide
实现代码以下:函数
import java.util.Iterator; /** * 能动态调整数组大小的栈 */ public class ArrayStack<E> implements Iterable<E>, Stack<E> { private E[] elements; private int size=0; @SuppressWarnings("unchecked") public ArrayStack() { elements = (E[])new Object[1]; //注意:java不容许建立泛型数组 } @Override public int size() {return size;} @Override public boolean isEmpty() {return size == 0;} //返回栈顶元素 @Override public E peek() {return elements[size-1];} //调整数组大小 public void resizingArray(int num) { @SuppressWarnings("unchecked") E[] temp = (E[])new Object[num]; for(int i=0;i<size;i++) { temp[i] = elements[i]; } elements = temp; } //入栈 @Override public void push(E element) { if(size == elements.length) { resizingArray(2*size);//若数组已满将长度加倍 } elements[size++] = element; } //出栈 @Override public E pop() { E element = elements[--size]; elements[size] = null; //注意:避免对象游离 if(size > 0 && size == elements.length/4) { resizingArray(elements.length/2);//小于数组1/4,将数组减半 } return element; } //实现迭代器, Iterable接口在java.lang中,但Iterator在java.util中 public Iterator<E> iterator() { return new Iterator<E>() { private int num = size; public boolean hasNext() { return num > 0; } public E next() { return elements[--num]; } }; } //测试 public static void main(String[] args) { int[] a = {1,2,3,4,new Integer(5),6};//测试数组 ArrayStack<Integer> stack = new ArrayStack<Integer>(); System.out.print("入栈顺序:"); for(int i=0;i<a.length;i++) { System.out.print(a[i]+" "); stack.push(a[i]); } System.out.println(); System.out.print("出栈顺序数组实现:"); //迭代 for (Integer s : stack) { System.out.print(s+" "); } } }
优势:性能
缺点:push()和pop()操做有时会调整数组大小,这项操做的耗时和栈的大小成正比测试
用一个数组来存储两个栈,让一个栈的栈底为数组的始端,即下标为0,另外一个栈的栈底为数组的末端,即下标为 n-1。两个栈若增长元素,栈顶都向中间延伸。其结构以下:
这种结构适合两个栈有相同的数据类型而且空间需求具备相反的关系的状况,即一个栈增加时另外一个栈缩短。如,买股票,有人买入,就必定有人卖出。
代码:
public class SqDoubleStack<E> { private static final int MAXSIZE = 20; private E[] elements; private int leftSize=0; private int rightSize= MAXSIZE - 1; //标记是哪一个栈 enum EnumStack {LEFT, RIGHT} @SuppressWarnings("unchecked") public SqDoubleStack() { elements = (E[])new Object[MAXSIZE]; //注意:java不容许建立泛型数组 } //入栈 public void push(E element, EnumStack es) { if(leftSize - 1 == rightSize) throw new RuntimeException("栈已满,没法添加"); if(es == EnumStack.LEFT) { elements[leftSize++] = element; } else { elements[rightSize--] = element; } } //出栈 public E pop(EnumStack es ) { if(es == EnumStack.LEFT) { if(leftSize <= 0) throw new RuntimeException("栈为空,没法删除"); E element = elements[--leftSize]; elements[leftSize] = null; //注意:避免对象游离 return element; } else { if(rightSize >= MAXSIZE - 1) throw new RuntimeException("栈为空,没法删除"); E element = elements[++rightSize]; elements[rightSize] = null; //注意:避免对象游离 return element; } } //测试 public static void main(String[] args) { int[] a = {1,2,3,4,new Integer(5),6};//测试数组 SqDoubleStack<Integer> stack = new SqDoubleStack<Integer>(); System.out.print("入栈顺序:"); for(int i=0;i<a.length;i++) { System.out.print(a[i]+" "); stack.push(a[i], EnumStack.RIGHT); } System.out.println(); System.out.print("出栈顺序数组实现:"); //迭代 for(int i=0;i<a.length;i++) { System.out.print(stack.pop(EnumStack.RIGHT)+" "); } } }
栈的链式存储结构,简称链栈。为了操做方便,通常将栈顶放在单链表的头部。一般对于链栈来讲,不须要头结点。
其存储结构以下图:
代码实现以下:
import java.util.Iterator; public class LinkedStack<E> implements Stack<E>, Iterable<E> { private int size = 0; private Node head = null;//栈顶 private class Node { E element; Node next; Node(E element, Node next) { this.element = element; this.next = next; } } @Override public int size() {return size;} @Override public boolean isEmpty() {return size == 0;} @Override public E peek() {return head.element;} @Override public void push(E element) { Node node = new Node(element, head); head = node; size++; } @Override public E pop() { E element = head.element; head = head.next; size--; return element; } //迭代器 public Iterator<E> iterator() { return new Iterator<E>() { private Node current = head; public boolean hasNext() { return current != null; } public E next() { E element = current.element; current = current.next; return element; } }; } public static void main(String[] args) { int[] a = {1,2,3,4,new Integer(5),6};//测试数组 LinkedStack<Integer> stack = new LinkedStack<Integer>(); System.out.print("入栈顺序:"); for(int i=0;i<a.length;i++) { System.out.print(a[i]+" "); stack.push(a[i]); } System.out.println(); System.out.print("出栈顺序链表实现:"); for (Integer s : stack) { System.out.print(s+" "); } } }
注意:私有嵌套类(内部类Node)的一个特色是只有包含他的类可以直接访问他的实例变量,无需将他的实例变量(element)声明为public或private。即便将变量element声明为private,外部类依然能够经过Node.element
的形式调用。
优势:
缺点:每一个元素都有指针域,增长了内存的开销。
顺序栈与链栈的选择和线性表同样,若栈的元素变化不可预料,有时很大,有时很小,那最好使用链栈。反之,若它的变化在可控范围内,使用顺序栈会好一些。
栈的一个最重要的应用就是递归。那么什么是递归呢?借用《哥德尔、艾舍尔、巴赫——集异璧之大成》中的话:
递归从狭义上来说,指的是计算机科学中(也就是像各位程序猿都熟悉的那样),一个模块的程序在其内部调用自身的技巧。若是咱们把这个效果视觉化就成为了「德罗斯特效应」,即图片的一部分包涵了图片自己。
以下面这张图,「先有书仍是先有封面 ?」
咱们把一个直接调用自身或经过一系列语句间接调用自身的函数,称为递归函数。每一个递归函数必须至少有一个结束条件,即不在引用自身而是返回值退出。不然程序将陷入无穷递归中。
一个递归的例子:斐波那契数列(Fibonacci)
\[ F(n)=\left\{ \begin{array}{lcl} 0, &{n = 0}\\ 1, &{n = 1}\\ F(n-1) + F(n-2),&{n > 1}\\ \end{array} \right. \]
递归实现:
public int fibonacci(int num) { if(num < 2) return num == 0 ? 0 : 1; return fibonacci(num - 1) + fibonacci(num - 2); }
迭代实现:
public int fibonacci(int num) { if(num < 2) return num == 0 ? 0 : 1; int temp1 = 0; int temp2 = 1; int result = 0; for(int i=2; i < num; i++) { result = temp1 + temp2; temp1 = temp2; temp2 = result; } return result; }
迭代与递归的区别:
编译器使用栈来实现递归。在前行阶段,每一次递归,函数的局部变量、参数值及返回地址都被压入栈中;退回阶段,这些元素被弹出,以恢复调用的状态。
队列是只容许在一端进行插入操做,在另外一端进行删除操做的线性表。它是一种基于先进先出(First In First Out,简称FIFO)策略的集合类型。容许插入的一端称为队尾,容许删除的一端称为队头。
抽象数据类型:
队列做为一种特殊的线性表,它同样包括插入、删除等基本操做。其基于泛型的API接口代码以下:
public interface Queue<E> { //队列是否为空 boolean isEmpty(); //队列的大小 int size(); //入队 void enQueue(E element); //出队 E deQueue(); }
一样的,队列具备两种存储方式:顺序存储和链式存储。
其存储结构以下图:
与栈不一样的是,队列元素的出列是在队头,即下表为0的位置。为保证队头不为空,每次出队后队列中的全部元素都得向前移动,此时时间复杂度为 O(n)。此时队列的实现和线性表的顺序存储结构彻底相同,不在详述。
若不限制队列的元素必须存储在数组的前n个单元,出队的性能就能大大提升。但这种结构可能产生「假溢出」现象,即数组末尾元素已被占用,若是继续向后就会产生下标越界,而前面为空。以下图:
解决「假溢出」的办法就是若数组未满,但后面满了,就从头开始入队。咱们把这种逻辑上首尾相连的顺序存储结构称为循环队列。
数组实现队列的过程:
假设开始时数组长度为5,如图,当f入队时,此时数组末尾元素已被占用,若是继续向后就会产生下标越界,但此时数组未满,将从头开始入队。当数组满(h入队)时,将数组的长度加倍。
代码以下:
import java.util.Iterator; /** * 能动态调整数组大小的循环队列 */ public class CycleArrayQueue<E> implements Queue<E>, Iterable<E> { private int size; //记录队列大小 private int first; //first表示头元素的索引 private int last; //last表示尾元素后一个的索引 private E[] elements; @SuppressWarnings("unchecked") public CycleArrayQueue() { elements = (E[])new Object[1]; } @Override public int size() {return size;} @Override public boolean isEmpty(){return size == 0;} //调整数组大小 public void resizingArray(int num) { @SuppressWarnings("unchecked") E[] temp = (E[])new Object[num]; for(int i=0; i<size; i++) { temp[i] = elements[(first+i) % elements.length]; } elements = temp; first = 0;//数组调整后first,last位置 last = size; } @Override public void enQueue(E element){ //当队列满时,数组长度加倍 if(size == elements.length) resizingArray(2*size); elements[last] = element; last = (last+1) % elements.length;//【关键】 size++; } @Override public E deQueue() { if(isEmpty()) return null; E element = elements[first]; first = (first+1) % elements.length;//【关键】 size--; //当队列长度小于数组1/4时,数组长度减半 if(size > 0 && size < elements.length/4) resizingArray(2*size); return element; } //实现迭代器 public Iterator<E> iterator() { return new Iterator<E>() { private int num = size; private int current = first; public boolean hasNext() { return num > 0; } public E next() { E element = elements[current++]; num--; return element; } }; } public static void main(String[] args) { int[] a = {1,2,4,6,new Integer(10),5}; CycleArrayQueue<Integer> queue = new CycleArrayQueue<Integer>(); for(int i=0;i<a.length;i++) { queue.enQueue(a[i]); System.out.print(a[i]+" "); } System.out.println("入队"); for (Integer s : queue) { System.out.print(s+" "); } System.out.println("出队"); } }
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,咱们简称为「链队列」。
存储结构以下图:
代码以下:
import java.util.Iterator; /** * 队列的链式存储结构,不带头结点的实现 */ public class LinkedQueue<E> implements Queue<E>, Iterable<E> { private Node first; //头结点 private Node last; //尾结点 private int size = 0; private class Node { E element; Node next; Node(E element) { this.element = element; } } @Override public int size() {return size;} @Override public boolean isEmpty(){return size == 0;} //入队 @Override public void enQueue(E element) { Node oldLast = last; last = new Node(element); if(isEmpty()) { first = last;//【要点】 }else { oldLast.next = last; } size++; } //出队 @Override public E deQueue() { E element = first.element; first = first.next; size--; if(isEmpty()) last = null;//【要点】 return element; } //实现迭代器 public Iterator<E> iterator() { return new Iterator<E>() { private Node current = first; public boolean hasNext() { return current != null; } public E next(){ E element = current.element; current = current.next; return element; } }; } public static void main(String[] args) { int[] a = {1,2,4,6,new Integer(10),5}; LinkedQueue<Integer> queue = new LinkedQueue<Integer>(); for(int i=0;i<a.length;i++) { queue.enQueue(a[i]); System.out.print(a[i]+" "); } System.out.println("入队"); for (Integer s : queue) { System.out.print(s+" "); } System.out.println("出队"); } }
循环队列与链队列,它们的基本操做的时间复杂度都为 O(1)。和线性表相同,在能够肯定队列长度的状况下,建议使用循环队列;没法肯定队列长度时使用链队列。
栈与队列,它们都是特殊的线性表,只是对插入和删除操做作了限制。栈限定仅能在栈顶进行插入和删除操做,而队列限定只能在队尾插入,在队头删除。它们均可以使用顺序存储结构和链式存储结构两种方式来实现。
对于栈来讲,若两个栈数据类型相同,空间需求相反,则可使用共享数组空间的方法来实现,以提升空间利用率。对于队列来讲,为避免插入删除操做时数据的移动,同时避免「假溢出」现象,引入了循环队列,使得队列的基本操做的时间复杂度降为 O(1)。