【数据结构】栈-CSDN博客

99 阅读3分钟

栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

栈也是「线性表」的一种,它的特点是:先进后出,后进先出,只允许在一端插入和删除元素

允许添加和删除元素的一端称作「栈顶」,另一端称作「栈底」。元素的添加操作被称作「入栈」,也叫「压栈」。元素的删除操作被称作「出栈」,也叫「弹栈」。
未命名文件 (18).jpg

1. 栈的实现

JDK内置了栈的实现,对应的类为java.util.Stack,底层采用数组来存储元素。

为了更好的理解栈的思想,这里我们自己实现一遍。栈的功能很简单,可以用数组实现,也可以用链表来实现。

1.1 数组存储

使用数组来存储元素,size属性用来记录元素的数量,不管是入栈还是出栈,都是操作size对应的元素。主要注意的是由于数组定长,一旦入栈的元素超出数组的上限,需要扩容。

public class Stack<T> {
	private int size;
	private Object[] table;

	public Stack() {
		table = new Object[16];
	}

	public void push(T data) {
		if (size == table.length) {
			// 扩容
			Object[] newTable = new Object[table.length << 1];
			for (int i = 0; i < table.length; i++) {
				newTable[i] = table[i];
			}
			table = newTable;
		}
		table[size++] = data;
	}

	public T pop() {
		if (size <= 0) {
			throw new RuntimeException("Stack is empty");
		}
		return (T) table[--size];
	}
}

1.2 链式存储

使用单向链表来存储元素,size属性用来记录元素的数量,入栈时采用头插法,操作的永远是链头元素。比数组的实现更加简单,不用担心扩容的问题。

public class LinkedStack<T> {
	private int size;
	private Node<T> head;

	public void push(T data) {
		Node<T> node = new Node<>(data, null);
		if (head == null) {
			head = node;
		}else {
			node.next = head;
			head = node;
		}
	}

	public T pop() {
		if (head == null) {
			throw new RuntimeException("Stack is empty");
		}
		try {
			return head.data;
		}finally {
			head = head.next;
		}
	}

	private class Node<T> {
		private T data;
		private Node<T> next;

		public Node(T data, Node<T> next) {
			this.data = data;
			this.next = next;
		}
	}
}

2. 栈的应用

根据栈先进后出的特点,这里列举几个栈的应用场景。

2.1 平衡符号

编译器将源代码编译成机器可识别的执行文件前,肯定都要做一个语法校验,语法都错误了,还咋编译啊。语法校验中,有一项几乎是所有语言都需要做的,那就是判断语法中开放符号和封闭符号是否平衡。例如左右括号必须成对匹配。

【分析】
初始化一个空栈,挨个字符遍历源代码,遇到开放符号则入栈,遇到封闭符号则出栈,判断符号是否一致,不一致则校验不通过。源码遍历结束后,判断栈是否空,如果空,则校验通过。

【实现】

public class BalanceSymbol {
	private static final Map<Character,Character> map;
	private char[] chars;
	private Stack<Character> stack;

	static {
		map = new HashMap<>(3, 1);
		map.put('(', ')');
		map.put('[', ']');
		map.put('{', '}');
	}

	public BalanceSymbol(String source) {
		this.chars = source.toCharArray();
		this.stack = new Stack<>();
	}

	public boolean check() {
		for (int i = 0; i < chars.length; i++) {
			char c = chars[i];
			if (c == '(' || c == '[' || c == '{') {
				stack.push(c);
			} else if (c == ')' || c == ']' || c == '}') {
				if (!map.get(stack.pop()).equals(c)) {
					throw new RuntimeException("不匹配的符号:" + c);
				}
			}
		}
		return stack.isEmpty();
	}

	public static void main(String[] args) {
		String source = "public class BalanceSymbol {" +
							"void test(){" +
								"int[] arr = new int[5];" +
								"System.out.println(arr[0]);" +
							"}" +
						"}";
		new BalanceSymbol(source).check();
	}
}

2.2 数据翻转

利用栈的「先进后出,后进先出」的特点,可以很轻松的实现数据的翻转。例如将字符串hello翻转为olleh

【分析】
初始化一个空栈,将要翻转的数组依次全部入栈,再将出栈的数据依次写回到数组中,就可以实现数据翻转。

【实现】

public class Reverse {
	private Stack stack = new Stack<>();

	// 翻转字符串
	public String reverse(String source) {
		char[] chars = source.toCharArray();
		for (int i = 0; i < chars.length; i++) {
			stack.push(chars[i]);
		}
		for (int i = 0; i < chars.length; i++) {
			chars[i] = (char) stack.pop();
		}
		return new String(chars);
	}

	// 翻转数组
	public void reverse(int[] source) {
		for (int i = 0; i < source.length; i++) {
			stack.push(source[i]);
		}
		for (int i = 0; i < source.length; i++) {
			source[i] = (int) stack.pop();
		}
	}
}

2.3 撤销和恢复

栈结构还可以很方便的用来实现操作的「撤销和恢复」功能。例如:浏览器的前进和后退、文档的撤销和恢复。

【分析】
初始化两个空栈:master和backup,当有新的操作时push到master。撤销操作将master出栈,backup入栈,保存撤销的数据。恢复操作将backup出栈,master入栈恢复数据。

【实现】

public class CancelRenew<T> {
	private Stack<T> master = new Stack();
	private Stack<T> backup = new Stack();

	public void push(T data) {
		master.push(data);
	}

	public void cancel() {
		if (master.isEmpty()) {
			throw new RuntimeException("暂不支持撤销");
		}
		backup.push(master.pop());
	}

	public void renew() {
		if (backup.isEmpty()) {
			throw new RuntimeException("暂不支持恢复");
		}
		master.push(backup.pop());
	}

	public void show() {
		Iterator<T> iterator = master.iterator();
		while (iterator.hasNext()) {
			System.out.println(iterator.next());
		}
	}

	public static void main(String[] args) {
		CancelRenew<Integer> cr = new CancelRenew<>();
		cr.push(1);
		cr.push(2);
		cr.push(3);
		cr.cancel();
		cr.show();
	}
}

3. 栈结构模拟算式计算过程

使用编程语言进行一个加减法的计算再简单不过了,但是你有没有想过,你写的代码字符是如何被计算机解析并执行的呢?如果给你一个算式字符串,你能够解析并执行吗?

【问题】
给定一个10以内的加减法数学算式字符串,使用程序解析这个算式,并计算得到算式结果。

【分析】
初始化一个空栈,遍历这个字符串,遇到操作数就入栈,遇到运算符则出栈和下一个操作数进行运算,再将结果入栈,最后栈顶的元素就是结果。

【实现】

public class MathCalc {
	private static Set<Character> operators;
	private Stack stack = new Stack();

	static {
		operators = new HashSet<>();
		operators.add('+');
		operators.add('-');
	}

	public int calc(String expression) {
		char[] chars = expression.toCharArray();
		for (int i = 0; i < chars.length; i++) {
			char c = chars[i];
			if (!operators.contains(c)) {
                // 操作数,入栈
				stack.push(c);
			} else {
                // 操作符,出栈前一个操作数
				int leftNum = Integer.parseInt(String.valueOf(stack.pop()));
				int rightNum = Integer.parseInt(String.valueOf(chars[++i]));
				if (c == '+') {
					stack.push(leftNum + rightNum);
				} else {
					stack.push((leftNum - rightNum));
				}
			}
		}
		return (int) stack.pop();
	}

	public static void main(String[] args) {
		int result = new MathCalc().calc("1+2+3+4-5");
		System.out.println(result);
	}
}

4. 总结

栈的特点是先进后出,后进先出,也叫作LIFO表,只能在栈顶添加和删除元素。栈的结构和功能都很简单,可以用数组或链表来实现。结合栈的特点,可以用它来实现数据翻转、数据的撤销和恢复等功能。最后用栈模拟了程序是如何解析算式并进行计算的,相信大家对栈这个数据结构有了一定的了解。