队列

什么是队列(Queue)?

队列(queue)是一种采用先进先出(FIFO,first in first out)策略的抽象数据结构。比如生活中排队,总是按照先来的先服务,后来的后服务。队列在数据结构中举足轻重,其在算法中应用广泛,最常用的就是在宽度优先搜索(BFS)中,记录待扩展的节点。

队列内部存储元素的方式,一般有两种,数组(array)和链表(linked list)。两者的最主要区别是:

数组对随机访问有较好性能。
链表对插入和删除元素有较好性能。

接下来我们试着用数组(array)和链表(linked list)来实现一下链表

用数组实现队列
在使用数组表示队列时,我们首先要创建一个长度为MAXSIZE的数组作为队列。

因为MAXSIZE是数组的长度,那MAXSIZE-1就是队列的最大下标了。

在队列中,除了用一组地址连续的存储单元依次存储从队首到队尾的元素外,还需要附设两个整型变量head和tail分别指示队首和队尾的位置。

我们主要介绍三个操作:

初始化队列
enqueue()向队尾插入元素
dequeue()删除并返回队首元素
每次元素入队时,tail向后移动;每次元素出队时,head向后移动。

我们可以将队列视作一个类,通过成员变量数组来表示一组地址连续的存储单元,再定义两个成员变量head和tail,将队列的基本操作定义成类的方法。(注意:为了将重点放在实现队列上,做了适当简化。示范队列仅支持整数类型,若想实现泛型,可用反射机制和object对象传参;此外,可多做安全检查并抛出异常)
java:

public class MyQueue {
	public int head, tail;
	public int MAXSIZE = 100000;
	public int[] queue = new int[MAXSIZE];

public MyQueue() {
    head = tail = 0;
    // do initialize if necessary
}

public void enqueue(int item) {      
    // 队列已满
    if(tail == MAXSIZE){
        return ;
    }

    queue[tail++] = item;
}

public int dequeue() {
    // 队列为空
    if (head == tail){
        return -1;
    }

    return queue[head++];      
}
}

是大家会发现,如果这样实现队列的话,我们考虑MAXSIZE为4的情况,如果我们采取下面的操作

enqueue(1)
enqueue(2)
enqueue(3)
enqueue(4)
dequeue()
dequeue()
结束后数组的状态时[^, ^, 3, 4], head = 2, tail = 4。('^'表示该位置为空,即当前元素已经出队)
从我们之前的判断来看,tail == MAXSIZE , 当前队列已经满了,不能继续添加元素了,但是实际上我们还可以继续添加元素。因此在使用数组实现队列时,可能会出现空间未有效利用的情况,因此,我们有两种解决方法:

使用链表来实现队列
使用数组来实现循环队列
那么我们就先来看用链表来实现队列的方法:

class Node {
	public int val;
	public Node next;
	public Node(int _val) {
		val = _val;
		next = null;
	}
}

public class MyQueue {
	public Node head, tail;

public MyQueue() {
    head = tail = null;
    // do initialize if necessary
}

public void enqueue(int item) {
    if (head == null) {
        tail = new Node(item);
        head = tail;        
    } else {
        tail.next = new Node(item);
        tail = tail.next;
    }
}

public int dequeue() {
    if (head != null) {
        int item = head.val;
        head = head.next;
        return item;
    }
    return -1;
}
}

可以发现链表可以轻松地避免“假溢出”的问题,因为在每次需要新增元素时,只需要新建一个ListNode就可以了。 当然,我们也可以用循环队列来解决这个问题,接下来我们就来看一下循环队列如何实现队列。

何自己用数组实现循环队列

队列是一种先进先出的线性表,它只允许在表的一端进行插入,而在另一端删除元素。允许插入的一端称为队尾,允许删除的一端称为队首。但是我们之前也提到了,数组实现的队列会导致“虽然数组没满,但是tail已经指向了数组末尾,返回数组已满,队列溢出的错误信号”,我们称之为“假溢出”。

为充分利用向量空间,克服"假溢出"现象的方法是:将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量。存储在其中的队列称为循环队列(Circular Queue)。循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。

我们主要介绍三个操作:

  • 初始化循环队列
  • enqueue()向队尾插入元素
  • dequeue()删除并返回队首元素
    在循环队列中,除了用一组地址连续的存储单元依次存储从队首到队尾的元素外,还需要附设两个整型变量head和tail分别指示队首和队尾的位置。

我们可以将循环队列视作一个类,通过成员变量数组来表示一组地址连续的存储单元,再定义两个成员变量head和tail,将循环队列的基本操作定义成类的方法,循环效果则用“模”运算实现,以此来实现循环队列。

每当tail到达末尾的时候,将tail对MAXSIZE取模,使其回到队首。但是如果这样我们会发现一个问题,队列为空和队列已满的条件都成了tail == head。

为了避免这种无法判断的情况,我们规定当循环队列只剩一个空位的时候,就认为队列已满。这样队列已满的条件就成了 (tail + 1) % MAXSIZE == head。

public class MyQueue {
	public int head, tail;
	public int SIZE = 4;
	public int[] queue = new int[SIZE];

	public MyQueue() {
		head = tail = 0;
		// do initialize if necessary
	}
	//压入一个元素
	public void enqueue(int item) {
		// 队列已满
		if ((tail + 1) % SIZE == head){
			return ;
		}

		queue[tail++] = item;
		tail %= SIZE;
	}
	//弹出一个元素
	public int dequeue() {
		// 队列为空
		if (head == tail){
			return -1;
		}

		int item = queue[head++];
		head %= SIZE;
		return item;

	}
}

比如在Java中我们就可以调用Queue接口来实现队列,给我们的代码带来许多便利。那接口到底是什么呢,我们一起来学习一下吧。

(这部分是Java语言专属的特性,其他语言的同学可以跳过)

虚类和接口,其实本质上来说,没有太大的区别。比如 python、C++ 这种语言里是不区分的。基本上是 Java 新搞出来的 一个概念。这种新的概念主要是让类的继承关系更加的“有据可依”,简单的说,区别主要是你只能继承自一个虚类,但是可以继承多个接口,因为继承自某个虚类,说明的是我“是”某个东西。比如 Human 继承自哺乳动物的虚类,那么 Human 就是哺乳动物,而接口的继承(或者实现 implements)指的是,我“有”某个功能,那么 Human 可以 implements 喝水的接口,还可以 implements 吃饭的接口。
因为在面向对象的发展过程中,原本只有父子类关系,后来有一些父类并不具备具体的实例,因此这些父类就会被定义为虚类,必须被一个子类继承才能实例化。比如哺乳动物,这玩意儿没法直接 new 一个,必须制定到底是个啥哺乳动物,是个猫还是个狗。
后来面向对象继续发展,就发现有一些方法大家可以共用,这样不用每个都去指定,比如哺乳动物和甲壳类动物,都可以吃饭和喝水,你说让他们都继承自一个虚类又不一定对,比如都继承自动物或者生物的虚类,但是你会发现可能有的生物不具备吃饭的功能。这个时候,就出现了接口,因为吃饭主要描述的是这个类能做的事情,最后,我们把大家公共的能做的事情抽象出来,做成了接口,但是你单独看接口,比如吃饭这个接口,你不可能去 new 一个吃饭的实例出来,因为他没有具体对应的实体。
所以这大概就是虚类和接口的区别了,虚类可以通过子类实例化,接口无法实例化,必须被其他类实现去描述他是怎么做某些行为的。虚类是类之间的属性共性抽象,接口是类的行为功共性抽象。
接下来我们来学习一下Java的接口(Interface),其他语言的同学可以跳过这一部分。

Java接口(Interface)是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为。打一个比方,接口好比一个戏中的角色,这个角色有一些特定的属性和操作,然后实现接口的类就好比扮演这个角色的人,一个角色可以由不同的人来扮演,而不同的演员之间除了扮演一个共同的角色之外,并不要求其它的共同之处。

接下来我们来介绍几个面试常用的Interface。

Set

Set注重独一无二,该体系集合可以知道某物是否已经存在于集合中,不会存储重复的元素。Set的实现类在面试中常用的是:HashSet 与 TreeSet

HashSet

  • 无重复数据

  • 可以有空数据

  • 数据无序

    Set set = new HashSet<>();
    for (int i = 1; i < 6; i ++) {
    set.add(i + "");
    }
    set.add("1"); //不会重复写入数据
    set.add(null);//可以写入空数据
    Iterator iter = set.iterator();
    while (iter.hasNext()) {
    system.out.print(iter.next() + " ");//数据无序
    }// 输出(无序)为 3 4 1 5 null 2

TreeSet

  • 无重复数据

  • 不能有空数据

  • 数据有序

      Set<String> set = new TreeSet<>();
      for (int i = 1; i < 6; i ++) {
      	set.add(i + "");
      }
      set.add("1"); //不会重复写入数据
      //set.add(null);//不可以写入空数据
      Iterator<String> iter = set.iterator();
      while (iter.hasNext()) {
      	system.out.print(iter.next() + " ");//数据有序
      }// 输出(有序)为 1 2 3 4 5
    

Map

Map用于存储具有映射关系的数据。Map中存了两组数据(key与value),它们都可以是任何引用类型的数据,key不能重复,我们可以通过key取到对应的value。Map的实现类在面试中常用是:HashMap 和 TreeMap.

HashMap

  • key 无重复,value 允许重复

  • 允许 key 和 value 为空

  • 数据无序

      public class Solution {
      	public static void main(String[] args){
      		Map<String, String> map = new HashMap<>();
      		for (int i = 5; i > 0; i --) {
      			map.put(i + "", i + "");
      		}
      		map.put("1","1");//key无重复
      		map.put("11","1");//value可以重复
      		map.put(null, null);//可以为空
      		for (Iterator i = map.keySet().iterator(); i.hasNext(); ) {
      			String key = (String)i.next();
      			String value = map.get(key);
      			System.out.println("key = " + key + ", value = " + value);
      		}
      	}
      }
    

//输出
/*
key = 11, value = 1
key = null, value = null
key = 1, value = 1
key = 2, value = 2
key = 3, value = 3
key = 4, value = 4
key = 5, value = 5
*/
//输出顺序与输入顺序无关

TreeMap

  • key 无重复,value 允许重复

  • key 不允许有 null,value 允许有 null

  • 有序(存入元素的时候对元素进行自动排序,迭代输出的时候就按排序顺序输出)

      public class Solution {
      	public static void main(String[] args){
      		Map<String, String> map = new TreeMap<>();
      		for (int i = 5; i > 0; i --) {
      			map.put(i + "", i + "");
      		}
      		map.put("1","1");//key无重复
      		map.put("11","1");//value可以重复
      		//map.put(null, null);//不可以为空
      		for (Iterator i = map.keySet().iterator(); i.hasNext(); ) {
      			String key = (String)i.next();
      			String value = map.get(key);
      			System.out.println("key = " + key + ", value = " + value);
      		}
      	}
      }
    

//输出
/*
key = 1, value = 1
key = 11, value = 1
key = 2, value = 2
key = 3, value = 3
key = 4, value = 4
key = 5, value = 5
*/
//输出顺序位String排序后的顺序

一个 List 是一个元素有序的、可以重复(这一点与Set和Map不同)、可以为 null 的集合,List的实现类在面试中常用是:LinkedList 和 ArrayList

LinkedList

基于链表实现

ArrayList

基于动态数组实现
LinkedList 与 ArrayList 对比:

对于随机访问get和set,ArrayList绝对优于LinkedList,因为LinkedList要移动指针
对于新增和删除操作add和remove,在已经得到了需要新增和删除的元素位置的前提下,LinkedList可以在O(1)的时间内删除和增加元素,而ArrayList需要移动增加或删除元素之后的所有元素的位置,时间复杂度是O(n)的,因此LinkedList优势较大

QueueQueue

队列是一种比较重要的数据结构,它支持FIFO(First in First out),即尾部添加、头部删除(先进队列的元素先出队列),跟我们生活中的排队类似。

PriorityQueue

基于堆(heap)实现
非FIFO(最先出队列的是优先级最高的元素)
普通 Queue

基于链表实现
FIFO

posted @ 2022-08-29 23:11  奋斗中的菲比  阅读(91)  评论(0编辑  收藏  举报