栈和队列

1.栈
1.1栈的概念和结构

栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素的操作。进行数据插入和删除的一端称为栈顶,另一端称为栈底。栈中数据元素遵循先进后出原则。
压栈:栈的插入操作叫做入栈,入数据在栈顶
出栈:栈的删除操作叫做出栈。出数据也在栈顶
示例1:基于数组的顺序栈


public class ArrayStack {
	private String[] items;// 数组
	private int count;// 栈中元素个数
	private int n;// 栈的大小
	// 初始化数组,申请一个大小为n的数组空间

	public ArrayStack(int n) {
		this.items = new String[n];
		this.count = 0;
		this.count = n;
	}

	// 入栈操作
	public boolean push(String item) {
		// 数组空间不够用了直接返回false
		if (count == n) {
			return false;
		}
		// 将item下标放到count位置,并且count加1
		items[count] = item;
		count++;
		return true;
	}

	// 出栈操作
	public String pop() {
		// 栈为空,则直接返回null
		if (count == 0) {
			return null;
		}
		// 返回下标为count-1的数组元素,并且栈中元素个数count-1
		String temp = items[count - 1];
		count--;
		return temp;
	}

}

示例2:基于链表的链式栈


public class StackBasedLinkedList {
	private static class Node {
		private int data;
		private Node next;

		public Node(int data, Node next) {
			this.data = data;
			this.next = next;
		}
	}

	private Node top = null;

//入栈
	public void push(int value) {
		Node newNode = new Node(value, null);
		// 判断栈是否空
		if (top == null) {
			top = newNode;
		}
		newNode.next = top;
		top = newNode;
	}

//出栈,用-1表示栈中没有数据
	public int pop() {
		if (top == null) {
			return -1;
		}
		int value = top.data;
		top = top.next;
		return value;
	}
	//打印
	public void printAll() {
		Node p=top;
		while(p!=null) {
			System.out.println(p.data+" ");
			p=p.next;
		}
		System.out.println();
	}
}

1.2支持动态扩容的顺序栈
上述基于数组实现的栈,是一个固定大小的栈,也就是说,在初始化栈时需要事先指定栈的大小。当栈满之后,就无法再往栈里添加数据了。尽管链式栈的大小不受限,但要存储next指针,内存消耗相对较多。要想实现一个支持动态扩容的栈,我们只需要底层依赖一个支持动态扩容的数组就可以了。当栈满之后,我们就申请一个更大的数组,将原来的数据搬移到新数组中。
在这里插入图片描述


import java.util.Arrays;

public class ArrayStack2 {
	private Object[] items;// 数组
	private int count;// 栈中元素个数
	private int n;// 栈的大小
//初始化数组,申请一个大小为n的数组空间

	public ArrayStack2(int n) {
		this.items = new Object[n];
		this.count = 0;
		this.count = n;
	}

//入栈操作
	public boolean push(Object item) {
		if (count == n) {
			int oldCount = n;
			int newCount = oldCount << 1;
			// 栈大小已经超过int的最大值
			if ((newCount + 8) - Integer.MAX_VALUE > 0) {
				return false;
			}
			// 数组扩容
			n = newCount;
			// 将item放到下标为count的位置上,并且count+1
			items = Arrays.copyOf(items, newCount);
		}
		items[count] = item;
		count++;
		return true;
	}

//出栈操作
	public Object pop() {
		// 栈为空,直接返回null
		if (count == 0) {
			return null;
		}
		// 返回下标count-1的数组元素,并且栈中元素个数count-1
		Object temp = items[count - 1];
		--count;
		return temp;
	}

	public static void main(String[] args) {
		ArrayStack2 stack = new ArrayStack2(1);
		stack.push(9);
		stack.push(6);
		stack.push(8);
		System.out.println(stack.pop());
	}
}

1.3栈的应用
1.4.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() 函数时,函数调⽤栈的情况。
在这里插入图片描述
1.4.2栈在表达式求值中的应用
为了⽅便解释,我将算术表达式简化为只包含加减乘除四则运算,⽐如:34+139+44-12/3。对于这个四则运算,我们⼈脑可以很快求解出答案,但是对于计算机来说,理解这个表达式本身就是个挺难的事⼉。如果换作你,让你来实现这样⼀个表达式求值的功能,你会怎么做呢?
实际上,编译器就是通过两个栈来实现的。其中⼀个保存操作数的栈,另⼀个是保存运算符的栈。我们从左向右遍历表达式,当遇到数字,我们就直接压⼊操作数栈;当遇到运算符,就与运算符栈的栈顶元素进⾏⽐较。如果⽐运算符栈顶元素的优先级⾼,就将当前运算符压⼊栈;如果⽐运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进⾏计算,再把计算完的结果压⼊操作数栈,继续⽐较。
我将 3+5
8-6 这个表达式的计算过程画成了⼀张图,你可以结合图来理解我刚讲的计算过程。
在这里插入图片描述
1.3.3栈在括号匹配中的应用
除了⽤栈来实现表达式求值,我们还可以借助栈来检查表达式中的括号是否匹配。
我们同样简化⼀下背景。我们假设表达式中只包含三种括号,圆括号 ()、⽅括号 [] 和花括号{},并且它们可以任意嵌套。⽐如,{[{}]}或 [{()}([])] 等都为合法格式,⽽{[}()] 或 [({)] 为不合法的格式。那我现在给你⼀个包含三种括号的表达式字符串,如何检查它是否合法呢?
这⾥也可以⽤栈来解决。我们⽤栈来保存未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时,则将其压⼊栈中;当扫描到右括号时,从栈顶取出⼀个左括号。如果能够匹配,⽐如“(”跟“)”匹配,“[”跟“]”匹配,“{”跟“}”匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为⾮法格式。当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明有未匹配的左括号,为⾮法格式。
2.队列
队列这个概念⾮常好理解。你可以把它想象成排队买票,先来的先买,后来的⼈只能站末尾,不允许插队。先进者先出,这就是典型的“队列”。
我们知道,栈只⽀持两个基本操作:⼊栈 push()和出栈 pop()。队列跟栈⾮常相似,⽀持的操作也很有限,最基本的操作也是两个:⼊队 enqueue(),放⼀个数据到队列尾部;出队 dequeue(),从队列头部取⼀个元素。
在这里插入图片描述
所以,队列跟栈⼀样,也是⼀种操作受限的线性表数据结构。
队列的概念很好理解,基本操作也很容易掌握。作为⼀种⾮常基础的数据结构,队列的应⽤也⾮常⼴泛,特别是⼀些具有某些额外特性的队列,⽐如循环队列、阻塞队列、并发队列。它们在很多偏底层系统、框架、中间件的开发中,起着关键性的作⽤。⽐如⾼性能队列 Disruptor、Linux 环形缓存,都⽤
到了循环并发队列;Java concurrent 并发包利⽤ ArrayBlockingQueue 来实现公平锁等。
2.队列的实现
2.1队列的概念和结构
跟栈⼀样,队列可以⽤数组来实现,也可以⽤链表来实现。⽤数组实现的栈叫作顺序栈,⽤链表实现的栈叫作链式栈。同样,⽤数组实现的队列叫作顺序队列,⽤链表实现的队列叫作链式队列。
示例1:基于数组的顺序队列


import java.util.Queue;

public class ArrayQueue<T> implements Queue<T> {
//存放具体数据
	private T[] elementData;
	// 队列头
	private int head;
	// 队列尾部
	private int tail;
	// 队列容量
	private int capacity;

	public ArrayQueue(int capacity) {
		this.capacity = (T[]) new Object[capacity];
	}

	// 元素入队
	public void enqueue(T t) {
		if (tail == capacity) {
			System.out.println("队列已满");
			throw new ArrayIndexOutOfBoundsException();
		}
		elementData[tail++] = t;
	}

	// 元素出队
	public T dequeue() {
		if (head == tail) {
			System.out.println("队列为空");
			throw new NullPointerException();
		}
		T result = elementData[head++];
		return result;
	}

	// 返回队首元素,但不出队
	public T peek() {
		if (head == tail) {
			System.out.println("队列为空");
			throw new NullPointerException();
		}
		T result = elementData[head];
		return result;
	}

	public int getSize() {
		return tail - head;
	}

	public boolean isEmpty() {
		return head == tail;
	}
}

上述入队函数enqueue实现有优化方式,解决随着不停地进⾏⼊队、出队操作,head 和 tail 都会持续往后移动。当 tail 移动到最右边,即使数组中还有空闲空间,也⽆法继续往队列中添加数据了的问题。
改造如下:

public void enqueue(T t) {
	if (tail == capacity) {
		// tail == capacity && head == 0 表示队列已满
		if (head == 0) {
			System.err.println("队列已满");
			throw new ArrayIndexOutOfBoundsException();
 		}
		else {
			// 数据搬移
			for (int i = head;i < tail;i++) {
				elementData[i - head] = elementData[i];
 			}
			// 数据搬移后更新两个指针位置
			tail -= head;
			head = 0;
		 }
	 }
	elementData[tail++] = t;
 }

这样可以看到:
当队列的 tail 指针移动到数组的最右边后,如果有新的数据⼊队,我们可以将
head 到 tail 之间的数据,整体搬移到数组中 0 到 tail-head 的位置。
在这里插入图片描述
示例2:基于链表的链式队列
基于链表的实现,我们同样需要两个指针:head 指针和 tail 指针。它们分别指向链表的第⼀个结点和最后⼀个结点。如图所示,⼊队时,tail->next= new_node, tail = tail->next;出队时,head = head->next。
在这里插入图片描述


public class QueueBasedOnLinkedList {
	private static class Node {
		private String data;
		private Node next;

		public Node(String data, Node next) {
			this.data = data;
			this.next = next;
		}

		public String getData() {
			return data;
		}
	}

//队列的队首和队尾
	private Node head = null;
	private Node tail = null;

//入队
	public void equeue(String value) {
		if (tail == null) {
			Node newNode = new Node(value, null);
			head = newNode;
			tail = newNode;
		} else {
			tail.next = new Node(value, null);
			tail = tail.next;
		}
	}

//出队
	public String dequeue() {
		if (head == null) {
			return null;
		}
		String value = head.data;
		head = head.next;
		if (head == null) {
			tail = null;
		}
		return value;
	}

	public void printAll() {
		Node p = head;
		while (p != null) {
			System.out.println(p.data + " ");
			p = p.next;
		}
		System.out.println();
	}
}

2.2支持动态扩容的顺序队列(有界)


package bittech.queue.impl;

import bittech.queue.Queue;

import java.util.Objects;

public class ArrayQueue<T> implements Queue<T> {
 	// 存放具体数据
 	private T[] elementData;
	 // 队列头
 	private int head;
 	// 队列尾部
 	private int tail;
 	// 队列容量
 	private int capacity;
 	public ArrayQueue(int capacity) {
		 this.capacity = capacity;
 		elementData = (T[]) new Object[capacity];
 	}
	 /**
 	* 元素⼊队
 	* @param t 要⼊队元素
	*/
 	@Override
 	public void enqueue(T t) {
		 if (tail == capacity) {
 			// tail == capacity && head == 0 表示队列已满
 			if (head == 0) {
 				System.err.println("队列已满");
				throw new ArrayIndexOutOfBoundsException();
 			}
 			else {
 				// 数据搬移
				 for (int i = head;i < tail;i++) {
					 elementData[i - head] = elementData[i];
				 }
				 // 数据搬移后更新两个指针位置
 				tail -= head;
 				head = 0;
			 }
 		}
		elementData[tail++] = t;
	 }
 	/**
 	* 元素出队
 	* @return 出队元素
	 */
	 @Override
 	public T dequeue() {
 		if (head == tail) {
			System.err.println("队列为空");
 			throw new NullPointerException();
 		}
 		T result = elementData[head++];
 		return result;
 	}
 	/**
 	* 返回队⾸元素但不出队
 	* @return
 	*/
 	@Override
	 public T peek() {
 		if (head == tail) {
 			System.err.println("队列为空");
 			throw new NullPointerException();
 		}
		T result = elementData[head];
 		return result;
 	}
	@Override
 	public int getSize() {
 		return tail - head;
 	}
 	@Override
 	public boolean isEmpty() {
 		return head == tail;
 	}
}

2.3循环队列
我们刚才⽤数组来实现队列的时候,在 tail==n 时,会有数据搬移操作,这样⼊队操作性能就会受到影响。那有没有办法能够避免数据搬移呢?我们来看看循环队列的解决思路。
循环队列,顾名思义,它⻓得像⼀个环。原本数组是有头有尾的,是⼀条直线。现在我们把⾸尾相连,扳成了⼀个环。如下图所示
在这里插入图片描述
我们可以看到,图中这个队列的⼤⼩为 8,当前 head=4,tail=7。当有⼀个新的元素 a ⼊队时,我们放⼊下标为 7 的位置。但这个时候,我们并不把 tail 更新为 8,⽽是将其在环中后移⼀位,到下标为 0 的位置。当再有⼀个元素 b ⼊队时,我们将 b 放⼊下标为 0 的位置,然后 tail 加 1 更新为 1。所以,在a,b 依次⼊队之后,循环队列中的元素就变成了下⾯的样⼦:
在这里插入图片描述
通过这样的⽅法,我们成功避免了数据搬移操作。看起来不难理解,但是循环队列的代码实现难度要⽐前⾯讲的⾮循环队列难多了。要想写出没有 bug 的循环队列的实现代码,我个⼈觉得,最关键的是,确
定好队空和队满的判定条件。
在⽤数组实现的⾮循环队列中,队满的判断条件是 tail == n,队空的判断条件是 head == tail。那针对循环队列,如何判断队空和队满呢?
队列为空的判断条件仍然是 head == tail。但队列满的判断条件就稍微有点复杂了。来看下⾯这张队列满的图,⼤家可以看⼀下,试着总结⼀下规律。
在这里插入图片描述
就像我图中画的队满的情况,tail=3,head=4,n=8,所以总结⼀下规律就是:(3+1)%8=4。多画⼏张队满的图,你就会发现,当队满时,

(tail+1)%n=head

你有没有发现,当队列满时,图中的 tail 指向的位置实际上是没有存储数据的。所以,循环队列会浪费⼀个数组的存储空间。
示例:用数组实现的循环链表


public class ArrayLoopQueue<T> implements Queue<T> {
	// 存放数据的泛型数组
	private T[] elementData;
	// 队⾸索引
	private int head;
	// 队尾索引
	private int tail;
	// 队列当前元素个数
	private int size;

	public ArrayLoopQueue(int capSize) {
		// 因为循环队列会浪费⼀个空间来判断当前队列是否已满,因此多开辟⼀块空间
		elementData = (T[]) new Object[capSize + 1];
	}

	public ArrayLoopQueue() {
		this(8);
	}

	@Override
	public void enqueue(T t) {
		// 判断队列是否已满
		if ((tail + 1) % elementData.length == head) {
			System.err.println("队列已满");
			throw new ArrayIndexOutOfBoundsException();
		}
		elementData[tail] = t;
		tail = (tail + 1) % elementData.length;
		size++;
	}

	@Override
	public T dequeue() {
		if (isEmpty()) {
			System.err.println("队列为空");
			throw new NullPointerException();
		}
		T result = elementData[head];
		elementData[head] = null;
		head = (head + 1) % elementData.length;
		size--;
		return result;
	}

	@Override
	public T peek() {
		if (isEmpty()) {
			System.err.println("队列为空");
			throw new NullPointerException();
		}
		T result = elementData[head];
		return result;
	}

	@Override
	public int getSize() {
		return size;
	}

	@Override
	public boolean isEmpty() {
		return head == tail;
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值