小编会一直更新数据结构相关方面的知识,使用的语言是Java,但是其中的逻辑和思路并不影响,如果感兴趣可以关注合集。
希望大家看完之后可以自己去手敲实现一遍,同时在最后我也列出一些基本和经典的题目,可以尝试做一下。大家也可以自己去力扣或者洛谷牛客这些网站自己去练习,数据结构光看不敲是学不好的,加油,祝你早日学会数据结构这门课程。
如果不是天才,就请一步一步来。
目录
队列
栈
相关题目
队列
概述
计算机科学中,队列(queue)是以顺序的方式维护的一组数据集合,规定只能在一端添加数据,在另一端移除数据。习惯来说,添加的一端称为队尾,移除的一端称为队头,就如同生活中的排队买商品,后面来排队的人都要排在队尾,哪怕你是超雄也不许插队,前面的人排在队头准备结账,结账走了就好比移除操作。
实现
队列可以通过链表实现也可以通过数组来实现,这里我两个都实现一下。
不管我用链表还数组来模拟实现队列,队列这种数据结构它所支持的操作是不变的,比如在队尾插入值,从队头删除值,获得队头的值还是检查队列是否为空等等,这些操作的性质和名称都是不变的,所以我们可以采用面向接口编程,这里先写好一个队列的接口,这样也方便我接下来的实现,接口也就定义了四个方法,都是队列所具备的基本操作,E是规定泛型,不局限于基本数据类型。
// 队列接口
public interface Queue<E> {
//向队列尾插入元素
boolean push(E value);
// 从队列头获取值并移除
// 如果队列为空则返回null
E poll();
// 获取队列头的值
E peek();
// 检查队列是否为空
boolean isEmpty();
}
链表实现
结构
这里我们采用单向环形带哨兵节点的链表来模拟实现队列,选择环形链表是弥补上一篇文章留下的环形链表的实现,带哨兵节点是有利于我们处理一些队列为空时的特殊情况。
我们首先来看一下单向环形链表的结构,节点类和之前都是一样的,由于是单向所以也就不需要prev指针。
// 节点类
private static class Node<E> {
E value;
Node<E> next;
public Node(E value, Node<E> next) {
this.value = value;
this.next = next;
}
}
接下来就是哨兵节点,我们需要设置一个哨兵节点head,但是我们还需要设置一个tail变量来指向链表的尾节点,因为这样可以使我们操作队列尾的时候更加方便,不需要从头遍历链表了。又因为是环形节点,一开始tail的next指针也要指向head,这些可以在构造函数中设置,因为哨兵节点随队列对象创建而创建。
// 链表实现队列
public class LinkedListQueue<E> implements Queue<E> {
private static class Node<E> {
E value;
Node<E> next;
public Node(E value, Node<E> next) {
this.value = value;
this.next = next;
}
}
Node<E> head;
Node<E> tail;
public LinkedListQueue() {
head = new Node<>(null, null);
tail = head;
tail.next = head;
}
}
添加元素
添加元素我们直接就是完善接口中的push方法就好了,对于在单向环形链表中我们添加元素,由于是在尾部添加,所以我们只需要将哨兵尾节点的next指向新节点cur,然后将新节点cur赋给tail更新哨兵尾节点就好了。
画的有点抽象,先看黑色再对着代码看红色的步骤。
//向队列尾插入元素
@Override
public boolean push(E value) {
// 1.先把cur的next指针指向head,构成环
Node<E> cur = new Node<>(value, head);
// 2.把tail节点和cur节点连起来
tail.next = cur;
// 3.更新cur节点为tail节点
tail = cur;
return true;
}
遍历
遍历队列实则就是遍历单向环形链表,链表的第一个节点从head的next节点开始,然后因为是环形,所以当我们再一次来到head节点是证明我们已经走完一圈了。简单直接上代码
// 遍历
public void forEach() {
// 因为哨兵节点没有意义,真正的第一个节点是哨兵的后一个节点
Node p = head.next;
// 链表成环,再一次来到head节点证明已经走过一圈
while (p != head) {
System.out.println(p.value);
p = p.next;
}
}
实现完遍历后我们就可以测试我们的添加方法了。
@Test
public void test01(){
LinkedListQueue<Integer> queue = new LinkedListQueue<>();
queue.push(1);
queue.push(2);
queue.push(3);
queue.push(4);
queue.push(5);
queue.forEach();
}
测试显示通过,打印与预期相同没有问题。
由于队列只允许操作头和尾,所以也就没有什么根据索引插入删除之类的操作,实现起来也是简单了许多。
队列是否为空
接下来我们来实现isEmpty这个方法,就是判断队列是否为空,这个方法特别简单,因为我们设置了一个tail变量来表示链表尾节点,只有当链表为空时tail才与head相等。
// 判断队列是否为空
@Override
public boolean isEmpty() {
return head == tail;
}
获取队列头元素
这个方法就是我们接口中的peek方法,这个方法要求我们返回队列头节点的值就行了,这也是非常节点,我们只要返回head的next指针指向的节点的值就行了。记得特殊处理队列为空的情况。
//获取队列头元素
@Override
public E peek() {
if (isEmpty()) {
// 队列为空返回null
return null;
}
return head.next.value;
}
测试一下也是没有问题的。
删除队头元素
poll方法与peek方法不同的是,poll方法还需要移除队头。删除一个链表的头节点相信你已经会了,head的next指针指向的就是待删除节点cur,我们只需要把head的next指向设置成cur的next指向的节点就好了,这样就把cur节点删除掉了。
这时我们再考虑一下特殊情况,当队列中只有一个节点,那么cur的next指向就是head(因为是环形链表)这时我们删除也是没有问题的,但是这时我们要改变tail的值,因为只有一个节点删除之后我们要将tail设置成head,而其它情况的删除我们不用更改,因为删除第一个节点与tail指向的最后一个节点没有关系。
//删除队列头元素
@Override
public E poll() {
if (isEmpty()) {
// 如果链表为空返回null
return null;
}
// 待删除节点cur
Node<E> cur = head.next;
// 删除cur节点
head.next = cur.next;
// 如果队列中只有一个节点特殊处理
if (cur == tail) {
tail = head;
}
return cur.value;
}
测试一下,没有问题。
用链表模拟实现队列我们也就完成了,另外有些地方会给队列设置一个size大小属性和capacity容量属性来表示队列有没有满,这里我就不带大家实现了,大家有兴趣可以自己加上去,这并没有多困难,相信你一定可以实现的。
环形数组实现
概述
环形数组就是一个首尾相连的数组,以前我们的数组是线性的,想象一下抽取其中一部分出来首尾相连也就构成了一个环形数组。主要优点就是不会存在空间浪费,性能更佳等。底层我们还是用一个数组来实现的,只是通过代码程序控制让它变成环形数组。
结构
环形数组的底层还是数组,另外由于我们模拟队列实现,所以我们还需要额外设置两个指针一个指向队列头,一个指向队列尾。另外我们还可以提供一个带参数的构造函数,由外界自己设置环形数组的大小,当然这里也可以我们自己设置,这都不是重点。
public class ArrayQueue<E> implements Queue<E> {
//环形数组
private E[] array;
//队列头指针
private int head = 0;
//队列尾指针
private int tail = 0;
//构造函数根据给的容量参数来创建数组
public ArrayQueue(int capacity) {
array = (E[]) new Object[capacity + 1];
}
}
队列是否为空
判断队列是否为空也是非常的简单,和链表实现一样,我们只需要判断head是否等于tail,因为只有当链表为空时head才等于tail。
//判断队列是否为空
@Override
public boolean isEmpty() {
return head == tail;
}
队列是否满
由于我们对外提供了一个设置容量的构造方法,所以我们在使用时也得注意不能当队列满了之后我们还继续添加元素。所以我们要判断一下队列是否满,在这之前我们要先学习一下环形数组如何求索引,非常简单我们在表示索引时都模上一个环形数组的长度就可以了,公式就是(index)%arr.length。接着我们来看看如何判断环形数组是否满,环形数组只有当tail指针的下一个索引为head指针时就表示环形数组已经满了。表示出来也就是判断表达式(tail+1)%arr.length == head。
//判断队列是否满
@Override
public boolean isFull() {
return (tail + 1) % array.length == head;
}
添加元素
往队列尾部新增元素其实我们只需要在当前尾指针tail指向的地方添加元素就行了,然后不要忘了更新尾指针,更新尾指针tail时由于是环形数组,我们+1后不要忘了模上一个数组的长度。
//队尾添加元素
@Override
public boolean push(E value) {
// 队列已经满了,特殊处理
if (isFull()) {
return false;
}
array[tail] = value;
tail = (tail + 1) % array.length;
return true;
}
遍历
遍历队列实则就是遍历我们的环形链表,我们也是和之前遍历的套路一样从队头head开始然后一直往后走,直到与队尾指针tail相等那我们就可以停止了。每次向前走都要模上一个数组的长度这是防止索引越界。
//遍历
public void forEach() {
int cur = head;
while (cur != tail) {
System.out.println(array[cur]);
cur = (cur + 1) % array.length;
}
}
我们在实现完遍历和添加元素后我们就可以测试一下了。
@Test
public void test02(){
ArrayQueue queue =new ArrayQueue(10);
queue.push(1);
queue.push(2);
queue.push(3);
queue.push(4);
queue.push(5);
queue.forEach();
}
测试显示通过同时打印也是我们的预期结果。
删除队头元素
在头部删除节点我们只需要将头指针head向后移动一位就代表我们删除了队头元素。同时不要忘了更新head时模上一个数组长度。
//队头删除元素
@Override
public E poll() {
// 队列为空,特殊处理
if (isEmpty()) {
return null;
}
E value = array[head];
head = (head + 1) % array.length;
return value;
}
测试一下也是没有问题的。
获取队列头部元素
这个方法实现起来更是简单,我们只需要返回头指针指向的元素就行了。
// 获取队头元素
@Override
public E peek() {
if (isEmpty()) {
return null;
}
return array[head];
}
测试一下没有问题。
同样用环形数组去实现队列也可以去新增两个变量,一个size表示大小,一个capacity表示容量,然后来判断队列是否空是否满等等,这个大家可以自己去实现一下,其实大同小异。
栈
概述
计算机科学中,栈stack是一种线性的数据结构,规定只能在其一端添加数据和移除数据。习惯来说,这一端称之为栈顶,另一端不能操作数据的称之为栈底,就如同生活中的一个杯子里面装满了球,只有先拿走上面的球才可以拿到下面的球。
和队列一样我们会用链表和数组两种方式模拟实现队列,为了接下来的实现方便我们还是先定义一个栈接口,里面先定义好几个栈的基本方法。E是规定泛型,不局限于基本数据类型。
public interface Stack<E> {
// 向栈顶添加元素
boolean push(E value);
// 从栈顶弹出元素并返回
E pop();
// 获取栈顶元素
E peek();
// 判断栈是否为空
boolean isEmpty();
// 判断栈是否满
boolean isFull();
}
链表实现
结构
这里我们用带哨兵节点的单向链表来实现。那栈的结构也就包含了一个哨兵头节点head,同时这里我们再给它设置一个capacity变量表示容量,一个size变量表示大小,来补一下队列中留的作业。
public class LinkedListStack<E> implements Stack<E> {
private int capacity; // 容量
private int size; // 大小
private Node<E> head; // 哨兵头节点
public LinkedListStack(int capacity) {
this.capacity = capacity;
head = new Node<>(null, null);
}
static class Node<E> {
E value;
Node<E> next;
public Node(E value, Node<E> next) {
this.value = value;
this.next = next;
}
}
}
这里讲一下因为栈不同于队列,栈只可以操作栈顶这一端,所以我们不用像队列一样设置两两个哨兵节点。
栈是否为空
这个其实很简单,因为我们有了size变量去表示栈中元素的个数,所以当size == 0时栈就是空的,又或者是当我们哨兵节点head的next指针指向空也代表我们栈是空的。
// 判断栈是否为空
@Override
public boolean isEmpty() {
return size == 0;
// return head.next == null;
}
栈是否满
这个也简单,size变量是表示栈中元素的个数,capacity变量是代表我们栈的容量,也就是表示栈可以装多少个元素,所以当size == capacity 时栈就是满的。
// 判断栈是否满。
@Override
public boolean isFull() {
return size == capacity;
}
添加元素
在栈顶添加元素,又因为我们是用链表模拟实现栈,所以就可以转换为在链表头添加元素的问题,相信这个问题对于你来说已经易如反掌了吧。我们只需要让原来哨兵头节点head的next的值赋给待添加节点cur的next,然后我们让head的next 指向cur就好了。不要忘了栈的大小+1。
// 在栈顶添加元素
@Override
public boolean push(E value) {
// 链表已满返回false 表示添加失败
if (isFull()) {
return false;
}
// 第一步让cur的next指向原来head的next
Node cur = new Node<E>(value, head.next);
// 第二步把head的next指向cur
head.next = cur;
// 不要忘了栈的大小+1
size++;
return true;
}
移除元素
照着上面添加元素的思路,其实移除栈顶元素,我们不难想到就是移除链表头节点。我们只需要让哨兵节点的next指向链表头节点cur的next就可以实现对cur的删除,不要忘了栈的大小-1。
// 移除栈顶元素
@Override
public E pop() {
// 链表为空返回null
if (isEmpty()) {
return null;
}
// 第一步拿到链表头节点cur
Node<E> cur = head.next;
// 第二步让head的next指向cur的next实现删除节点cur。
head.next = cur.next;
// 不要忘了栈的大小-1
size--;
return cur.value;
}
获取栈顶元素
这个更是简单,我们只需要返回链表头元素的值就好了,也就是head.next.value。
// 获取栈顶元素
@Override
public E peek() {
// 链表为空,返回null
if (isEmpty()) {
return null;
}
return head.next.value;
}
遍历
遍历栈我们是从栈顶遍历到栈底,那么也就是遍历整条链表,这对你来说也是简简单单好吧。
//遍历
public void forEach() {
Node<E> cur = head.next;
while (cur != null) {
System.out.println(cur.value);
cur = cur.next;
}
}
测试代码和测试结果就不贴了啊,你们自己敲完测试一下就行了。
数组实现
结构
用数组来模拟实现栈我们肯定是需要一个数组array的,接着呢我们还需要一个变量top表示我们的栈顶指针,这就相当于我们用链表实现的head,top在数组中的位置一直指向栈顶的后一位。
public class ArrayStack<E> implements Stack<E> {
private E[] array;
private int top; // 栈顶指针
public ArrayStack(int capacity) {
this.array = (E[]) new Object[capacity];
}
}
栈是否为空
因为栈顶指针一直指向我们的栈顶后一位,那当我们栈顶指针指向0时也就表示我们此时栈为空。
// 判断栈是否为空
@Override
public boolean isEmpty() {
return top == 0;
}
栈是否满
这个其实也简单,当我们栈顶指针top与数组长度arr.length相等时就表示栈是满的。
// 判断栈是否满。
@Override
public boolean isFull() {
return top == array.length;
}
添加元素
现在我们使用的是数组模拟链表那我们添加元素就更简单了,我们只需要在数组arr中的top位置处赋上新元素的值接着让栈顶指针往后走一步,也就是+1就可以了。
// 在栈顶添加元素
@Override
public boolean push(E value) {
// 链表已满返回false 表示添加失败
if (isFull()) {
return false;
}
array[top] = value;
top++;
return true;
}
删除元素
对于用数组模拟实现的栈,我们不用向链表一样去删除那个节点(那块内存),因为数组的内存是连续的我们没法去删除其中一块,我们只能实现值覆盖来达到删除的目的,我们可以把栈顶指针往前移动,这样在下一次添加其它元素时会自动覆盖原来的值,这样也就意味着实现了删除。
// 移除栈顶元素
@Override
public E pop() {
// 链表为空返回null
if (isEmpty()) {
return null;
}
// 拿到栈顶元素的值
E value = array[top - 1];
top--;
return value;
}
获取栈顶元素
这个和我们删除栈顶元素大同小异,只不过不用把栈顶指针往后移动。
// 获取栈顶元素
@Override
public E peek() {
if (isEmpty()) {
return null;
}
return array[top - 1];
}
遍历
由于栈是从栈顶往栈底遍历,所以我们遍历时也是从数组右边往左边遍历就好了。
//遍历
public void forEach() {
for (int i = top - 1; i >= 0; i--) {
System.out.println(array[i]);
}
}
自己敲完测试一下就行了。
相关题目
20. 有效的括号 - 力扣(LeetCode)
933. 最近的请求次数 - 力扣(LeetCode)
2073. 买票需要的时间 - 力扣(LeetCode)
234. 回文链表 - 力扣(LeetCode)
1614. 括号的最大嵌套深度 - 力扣(LeetCode)
150. 逆波兰表达式求值 - 力扣(LeetCode)
带着决心起床,带着满意入睡。