[入门必看]数据结构3.2:队列

Posted H3T

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[入门必看]数据结构3.2:队列相关的知识,希望对你有一定的参考价值。

[入门必看]数据结构3.2:队列


第三章 栈、队列和数组

小题考频:23
大题考频:4


3.2 队列

难度:☆☆☆

知识总览

3.2.1_队列的基本概念

3.2.2_队列的顺序实现

3.2.3_队列的链式实现

3.2.4_双端队列



3.2.1_队列的基本概念

数据结构三要素——逻辑结构、数据的运算、存储结构(物理结构)

存储结构不同,运算的实现方式不同


队列(Queue)只允许在一端进行插入,在另一端删除线性表

逻辑结构:与普通线性表相同
数据的运算:插入、删除操作有区别

Queue排队:

从队尾插入,从队头删除。

  • 重要术语:队头、队尾、空队列

队列的基本操作

InitQueue(&Q):初始化队列,构造一个空队列Q。
DestroyQueue(&Q):销毁队列。销毁并释放队列Q所占用的内存空间

创、销

EnQueue(&Q,x):进栈,若队列Q未满,将x加入,使之成为新的队尾
DeQueue(&Q,&x):出栈,若队列Q非空,删除队头元素,并用x返回。

增、删;
返回栈顶元素、删除栈顶元素

GetHead(Q,&x):读队头元素。若栈S非空,则用x返回栈顶元素

查:栈的使用场景中大多只访问栈顶元素;
返回栈顶元素、不删除栈顶元素

其他常用操作:
StackEmpty(S):判断一个栈S是否为空。若S为空,则返回true,否则返回false。


3.2.2_队列的顺序实现

初始化操作

  • 队列结构体:
#define MaxSize 10 //定义队列中元素的最大个数
typedef struct
	ElemType data[Maxsize] //用静态数组存放队列元素
	int front,rear; //队头指针和队尾指针
 SqQueue; //Sq:sequence - 顺序

void testQueue()
	SqQueue Q; //声明一个队列(顺序存储)
	//……

队头指针指向队头元素,队尾指针指向队尾元素的后一个位置

  • 初始化队列:
//初始化队列
void InitQueue(SqQueue &Q)
	//初始化时,队头、队尾指针指向0
	Q.rear = Q.front = 0; 

  • 判断队列为空:
bool QueueEmpty(SqQueue Q)
	if(Q.rear == Q.front) //队空
		return true;
	else //不空
		return false;


入队操作

——只能从队尾入队(插入)

  • 新元素入队:
//新元素入队
bool EnQueue(SqQueue &Q, ElemType x)
	if(队列已满) //队满,报错
		return false; 
	Q.data[Q.rear] = x; //将x插入队尾
	Q.rear = (Q.rear + 1)%MaxSize; //队尾指针加1取模
	return true; 

队列已满的条件:rear==MaxSize?????????
错!
当队头元素出队后,前面的位置空闲了,可以继续入队新元素。

  • Q.rear = (Q.rear + 1)%MaxSize;
    0,1,2,…,MaxSize - 1将存储空间在逻辑上变成了“环状”

取模运算,即取余运算。两个整数a,b,a%b == a除以b的余数
在《数论》中,通常表示为a MOD b

模运算将无限的整数域映射到有限的整数集合0,1,2,…,b - 1上;
模运算将存储空间在逻辑上变成了“环状”


循环队列 - 入队操作

队列已满的条件:队尾指针的再下一个位置是队头,即
(Q.rear+1)%MaxSize= = Q.front

如果再插入一个数据元素,rear和front指针指向同一个位置 - 是判断队空的条件,所以不可以再插入数据元素了。
代价:牺牲一个存储单元

代码实现:

//判断队列是否为空
bool QueueEmpty(SqQueue Q)
	if(Q.rear == Q.front) //队空条件
		return true;
	else
		return false;


//入队
bool EnQueue(SqQueue &Q,ElemType x)
	if((Q.rear + 1) % Maxsize == Q.front)
		return false; //队满则报错
	Q.data[Q.rear] = x; //新元素插入队尾
	Q.rear = (Q.rear + 1) % Maxsize; //队尾指针加1取模 - 用模运算将存储空间在逻辑上变成了“环状”
	return true;


循环队列 - 出队操作


删、查代码实现:

//出队(删除一个队头元素,并用x返回)
bool DeQueue(SqQueue &Q,ElemType &x)
	if(Q.rear == Q.front)
		return false; //队空则报错
	x = Q.data[Q.front];
	Q.front = (Q.front + 1) % MaxSize; //队头指针后移
	return true; 


//获得队头元素的值,用x返回
bool GetHead(SqQueue Q,ElemType &x)
	if(Q.rear == Q.front)
		return false;//队空则报错
	x=Q.data[Q.front];
	return true;

方案一:判断队列已满/已空

用队尾指针和队头指针的值计算出这个队列当中当前有多少个数据元素:

(rear + MaxSize - front) % MaxSize


初始化时,队尾指针和队头指针都指向同一结点

队空条件为:Q.rear == Q.front,即队尾指针和队头指针都指向同一结点。

队满条件为:(Q.rear+1)%MaxSize == Q.front,即队尾指针的再下一个位置是队头,此时就浪费了一个存储空间。

刁难!不允许浪费存储空间!
 
这种情况下,判断队空和队满的条件都是队尾指针和队头指针都指向同一结点,如何区分?


方案二:判断队列已满/已空

在队列结构中定义一个变量size,用来记录队列中存放了几个数据元素,开始时size的值设为0。

typedef struct
	ElemType data[MaxSize];
	int front, rear;
	int size; //队列当前长度
 SqQueue;

队列元素的个数 = size

  • 初始化时rear = front = 0;
    size = 0;
  • 插入成功 size++;
    删除成功 size–;


虽然栈满和栈空时,队头指针和队尾指针都是指向同一个位置,但是由于定义了变量size,则可以用size来判断队满还是队空:

  • 队满时,size == MaxSize;
    队空时,size == 0;

方案三:判断队列已满/已空

还可以定义一个变量tag,当tag值为0时,表示最近执行过一次删除操作,当tag值为1时,表示最近执行过一次插入操作,开始时tag的值设为0。

typedef struct
	ElemType data[MaxSize];
	int front, rear;
	int tag; //最近进行的是删除/插入
 SqQueue;
  • 初始化时rear = front = 0;
    tag = 0;
  • 每次删除操作成功时,都令tag = 0;
    每次插入操作成功时,都令tag = 1;

只有删除操作,才可能导致队空
只有插入操作,才可能导致队满

  • 如果是由于插入操作(tag = 1)导致队头队尾指针指向同一位置,此时队满:
    队满条件为:front == rear && tag == 1
  • 如果是由于删除操作(tag = 0)导致队头队尾指针指向同一位置,此时队空:
    队空条件为:front == rear && tag == 0

其他方法

——以上方法都是基于队尾指针指向队尾元素的下一个位置的前提条件

  • 实际情况中,也有可能遇到队尾指针是指向队尾元素的情况。

这种情况下,入队操作时,先让队尾指针往后移一位,再将新的数据元素x插入队列。

所以初始化时,比较合理的方式是让front指针指向0这个位置;
让rear指针指向n-1位置:

此时插入第一个数据元素时,先让rear指针往后移一位,指向0,然后往这个位置插入新的数据元素x

  • 该设计方式判断队列为空的方法为:判断rear指针的下一个位置是不是front

那么该设计方式判断队列为满的方法不能使用:
(Q.rear+1)%MaxSize == Q.front 因为与队列为空相同。

合理的判满方法为:

  • 方案一:牺牲一个存储单元
    规定front指针前一个存储单元不可以存放数据元素,队满和队空时,两个指针的相对位置不同,可以区分。
  • 方案二:增加辅助变量
    size或tag,同上

3.2.3_队列的链式实现


——只能分别在队尾和队头进行增删操作的单链表(青春版),其也有带头结点的版本和不带头结点的版本。

代码定义队列:

typedef struct LinkNode //链式队列结点
	ElemType data;
	struct LinkNode *next;
LinkNode;

typedef structi //链式队列
	LinkNode *front, *rear; //队列的队头和队尾指针
LinkQueue;

入队时,用一个专门的尾指针指向最后一个结点,就不用从头往后寻找。
出队时,直接从头结点找到第一个数据结点,并删除即可。


初始化(带头结点)

typedef struct LinkNode
	ElemType data;
	struct LinkNode *next;
LinkNode;

typedef struct
	LinkNode *front,*rear;
LinkQueue;

//初始化队列(带头结点)
void InitQueue(LinkQueue &Q)
	//初始时front、rear都指向头结点
	Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
	Q.front->next = NULL;

//判断队列是否为空
bool IsEmpty(LinkQueue Q)
	if(Q.front == Q.rear)
		return true;
	else
		return false;


void testLinkQueue()
	LinkQueue Q; //声明一个队列
	InitQueue(Q); //初始化队列
	//……

  • 判空:
    front指针和rear指针指向同一个结点,
    或者头结点的next指针是否指向NULL

初始化(不带头结点)

//初始化队列(不带头结点)
void InitQueue(LinkQueue &Q)
	//初始时front、rear都指向NULL
	Q.front=NULL;
	Q.rear=NULL;


//判断队列是否为空(不带头结点)
bool IsEmpty(LinkQueue Q)
	if(Q.front==NULL)
		return true;
	else
		return false;

  • 判空:
    front指针是否等于NULL,
    或rear指针是否等于NULL

入队(带头结点)


入队(不带头结点)


不带头节点,第一个元素入队的时候要特别处理,让front指针和rear指针都指向第一个结点。

接下来对rear指针指向的结点进行一个后插操作。
每次插入结点之后,都要让rear指针指向新的表尾结点。


出队(带头结点)


出队表尾结点需要特殊处理,需要修改表尾指针,指向头结点,让rear让front指向同一个位置,表示队列变成了空队列。


出队(不带头结点)


没有头结点,每次出队都要修改front指针指向。
最后一个结点出队之后,也要让front和rear都指向NULL,恢复成空队。


队列满的条件


顺序存储 - 静态数组空间有限;
连式存储 - 一般不会队满,除非内存不足


3.2.4_双端队列

演示

  • 栈的演示:
  • 队列的演示:
  • 双端队列的演示:

对于双端队列,若只使用其中一端的插入、删除操作,则效果等同于栈。

  • 双端队列的两个变种:

输入受限和输出受限

考查重点:判断输出序列合法性

Q:若数据元素输入序列为1,2,3,4,则哪些输出序列是合法的,哪些是非法的?

四个元素进行排列组合,总共可能会有24种输出顺序。

1,2,3,42,1,3,43,1,2,44,1,2,3
1,2,4,32,1,4,33,1,4,24,1,3,2
1,3,2,42,3,1,43,2,1,44,2,1,3
1,3,4,22,3,4,13,2,4,14,2,3,1
1,4,2,32,4,1,33,4,1,24,3,1,2
1,4,3,22,4,3,13,4,2,14,3,2,1
  1. 使用栈,有多少种合法序列:
1,2,3,4 2,1,3,43,1,2,44,1,2,3
1,2,4,3 2,1,4,33,1,4,24,1,3,2
1,3,2,42,3,1,43,2,1,44,2,1,3
1,3,4,22,3,4,13,2,4,1 4,2,3,1
1,4,2,32,4,1,33,4,1,24,3,1,2
1,4,3,22,4,3,13,4,2,14,3,2,1

卡特兰数:
1 n + 1 C 2 n n = 1 4 + 1 C 8 4 = 14 \\frac1n+1C_2n^n=\\frac14+1C_8^4=14 n+11C2nn=4+11C84=14

14种合法出栈序列
输出某个序号元素时,在其之前的所有元素都已输入队列。

  1. 使用输入受限的双端队列,有多少种合法序列:

栈中合法的序列,双端队列中一定也合法

1,2,3,4 2,1,3,43,1,2,44,1,2,3
1,2,4,3 2,1,4,33,1,4,24,1,3,2
1,3,2,42,3,1,43,2,1,44,2,1,3
1,3,4,22,3,4,13,2,4,1 4,2,3,1
1,4,2,32,4,1,33,4,1,24,3,1,2
1,4,3,22,4,3,13,4,2,14,3,2,1

输出某个序号元素时,在其之前的所有元素都已输入队列。

  1. 使用输出受限的双端队列,有多少种合法序列:

栈中合法的序列,双端队列中一定也合法

1,2,3,4 2,1,3,43,1,2,44,1,2,3
1,2,4,3 2,1,4,33,1,4,24,1,3,2
1,3,2,42,3,1,43,2,1,44,2,1,3
1,3,4,22,3,4,13,2,4,1 4,2,3,1
1,4,2,32,4,1,33,4,1,24,3,1,2
1,4,3,22,4,3,13,4,2,14,3,2,1

知识回顾与重要考点

3.2.1_队列的基本概念

  • 只能在队尾插入、在队头删除,先进先出(FIFO)

3.2.2_队列的顺序实现

  • 用静态数组存放队列
  • 静态数组容量有限,用模运算(取余)重复利用静态数组中各空闲的存储空间,将存储空间在逻辑上变为“环状”。

容易在选择题中考查:

  • 1、rear指针指向
    ①. 队尾元素后一个位置
    ②. 队尾元素

  • 2、所给条件应该如何判断队空和队满。
    a. 牺牲一个存储单元
    b. 增加size变量记录队列长度
    c. 增加tag=0/1用于标记

  • 思考:分别采用
    ①a、①b、①c
    ②a、②b、②c
    策略时,如何实现以下操作:
    1、初始化、入队、出队;
    2、判空、判满;
    3、计算队列长度


3.2.3_队列的链式实现

  • 入队只能在队尾;
    出队只能在队头
  • 首先关注是否带头结点
  • 第一个元素入队和最后一个元素出队可能有特殊处理
    特别注意要修改rear指针
  • 计算长度:从队头结点遍历,统计一共有多少结点
    时间复杂度为 O ( n ) O(n) O(n)
    如果频繁需要使用长度,加入一个int型变量length用来记录这个队列到底有多少个元素(合理的改造)

3.2.4_双端队列

——一种操作受限的线性表

  • 双端队列是队列的变种
  • 回忆:栈的变种——共享栈

前端必看js数据结构与算法(队列,链表,集合,字典,树,图,堆)

队列

队列简介

  • 一个先进先出的数据结构
  • js中没有队列,但是可以用Array实现对队列的所有功能


使用数组模拟先进先出的场景

const queue = [] 
// 进队列
queue.push(1)
queue.push(2)

// 出队列
const itme1 = queue.shift()
const itme2 = queue.shift()


什么时候用

  1. 食堂排队打饭
  2. 所有先进先出的场景
  3. js 异步中的任务队列
    一个leetcode题 第933题

链表

链表是什么

  • 多个元素组成的列表
  • 匀速存储不连续, 用next指针连在一起

数组 vs 链表

  • 数组:增删非收尾元素是往往需要移动元素
  • 链表:增删非收尾元素,不需要移动元素,只需要更改next指针即可

js中的链表

  • js没有链表的数据结构
  • 可以用Object模拟链表

const a = { val: 'a' }
const b = { val: 'b' }
const c = { val: 'c' }
const d = { val: 'd' }
a.next = b
b.next = c
c.next = d

// 遍历
let point = a
while (point) {
    console.log(point.val)
    point = point.next
}

// 插入
(c-d)中插入d

const e = {val:'e'}
c.next = e
e.next = d


// 删除 (删除e)
c.next = d

leetcode练习:第83题-删除链表重复元素

var deleteDuplicates = function(head) {
  // 定义链表的一个头部的指针
  let p = head
  while(p && p.next) {
      if(p.val === p.next.val) {
        // 删除链表的一项
          p.next = p.next.next
      }else {
      // 不相同的时候再移动指针
           p = p.next
      }
  }
  return head
 
};

function myInstanceof (A, B) {
  // 遍历链表
  let p =  A
  while (p) {
    p = p.__proto__
    // B的 prototype 属性是否出现在A实例对象的原型链上
    if (p === B.prototype) {
      return true
    }
   
  }
  return false
}
function Foo () {}
var f = new Foo()
console.log(myInstanceof(f, Foo)); // true
console.log(myInstanceof(f, Object)); // true

集合

集合简介

  • 一种无序且唯一的数据结构
  • ES6中有集合, 名为Set
  • 集合常用操作: 去重,判断某元素是否在集合中,求交集
// 去重
const arr = [1,1,2,3,4,3]
const arr2 = [...new Set(arr)]

// 判断元素是否在集合中
let set = new Set(arr)

// add 方法
set.add(1)
set.add('text')
set.add({a:1,b:2})

// has方法
const has =set.has(3)

// delete方法
set.delete(1)

// 获取size 方法
console.log(set.size)
// 求交集
const set2 = new Set([2,3])
const set3 new Set([...set]).filter(item => set2.has(item))

// 求差集
const set2 = new Set([2,3])
const set4 = new Set([...set]).filter(item => !set2.has(item))

// 数组转为set
set2 = new Set([1,2,3])

// 迭代方法 fot ..of
for (let item of set) console.log(item)
for (let item of set.keys())) console.log(item)
for (let item of set.values()) console.log(item)
for (let item of set.entrise()) console.log(item)

补充说明迭代
内置迭代器:

可迭代的对象,都内置以下3种迭代器

entries(): 返回一个迭代器,值为键值对

values(): 返回一个迭代器, 值为集合的值

keys(): 返回一个迭代器,值为集合中的所有键

let userList = [ 'ghostwu', '悟空', '八戒' ];

for ( let name of userList.entries() ) {
    console.log( name );
}

let set = new Set( [ 10, 20, 30 ] );
for ( let num of set.entries() ){
    console.log( num );
}

let map = new Map( [ [ 'name', 'ghostwu' ], [ 'age', 22 ] ] );
for ( let detail of map.entries() ){
    console.log( detail );
}

字典

字典简介

  • 与集合相似, 字典也是一种存储为一值的数据结构, 但他是以键值对的形式存储
  • ES6中有字典–>Map(映射)
  • 常见操作 增(set) 删(delete) 改(set) 查(get)
const m = new Map()

//增
m.set('a','aaa')

// 删
m.delete('a')
m.clear()

// 改
m.set('a','aaaaa')

// 查
m.get('a')

使用Map取两个数组的交集

var intersection = function(nums1, nums2) {
  // new Set(nums1) 去重
  return  [...new Set(nums1)].filter(item => nums2.includes(item))
};

树简介

  • 一种分层数据的抽象模型
  • 前端工作中常见的树包括:DOM树,级联选择,树形控件…
  • js中没有树,但是可以用Array 和Object构建树
  • 树的常用操作: 深度/广度优先遍历 , 先中后序遍历

树的深度/广度优先遍历

  • 深度优先遍历: 尽可能深的搜索树的分支:递归
    1. 访问根节点
    2. 对根节点的children挨个进行深度优先遍历
const tree = {
  val: 'a',
  children: [
    {
      val: 'b',
      children: [
        {
          val: 'd',
          children: [
            
          ]
        },
        {
          val: 'e',
          children: [
            
          ]
        }
      ]
    },
    {
      val: 'c',
      children: [
        {
          val: 'f',
          children: [
            
          ]
        },
        {
          val: 'g',
          children: [
            
          ]
        }
      ]
    }
  ]
}
const dfs =(root) => {
  console.log(root.val)
  root.children.forEach(dfs)
}

dfs(tree)

打印结果

  • 广度优先遍历:先访问离根节点最近的节点
    1. 新建一个队列, 把根节点入队
    2. 把对头出队并访问
    3. 把对头的children挨个入队
    4. 重复第二,第三,直到队列为空



const bfc = (root) => {
  const q = [root]
  while (q.length > 0) {
    const n = q.shift()
    console.log(n.val)
    n.children.forEach(child => {
      q.push(child)
    })
  }
}

打印结果:

二叉树的先中后序遍历

二叉树是什么?

  • 树中每个节点最多只能有两个子节点

  • 在js中通常用Object来模拟二叉树

const binaryTree = {
  val: 1,
  left: {
    val:2,
    left: null,
    right: null
  },
  right: {
    val:3,
    left: null,
    right: null
  }
}

先序遍历算法

  1. 访问节点
  2. 对根节点的子树进行先序遍历
  3. 对根节点的子树进行先序遍历

遍历顺序如图

定义一棵树


const binaryTree = {
 val: 1,
 left: {
   val: 2,
   left: {
     val: 4,
     left: null,
     right: null,
   },
   right: {
     val: 5,
     left: {
       val: 7,
       left: null,
       right: null,
     },
     right: null,
   },
 },
 right: {
   val: 3,
   left: null,
   right: {
     val: 6,
     left: null,
     right: null,
   },
 },
};

递归版:

const preorder = root => {
  if (!root) return;
  console.log(root.val);
  preorder(root.left);
  preorder(root.right);
};
preorder(binaryTree);

非递归(栈特性):

const preorder = root => {
  if (!root) return;
  const stack = [root];
  while (stack.length) {
    const n = stack.pop();
    console.log(n.val);
    n.right && stack.push(n.right);
    n.left && stack.push(n.left);
  }
};
preorder(binaryTree);

打印结果:

中序遍历算法

  1. 对根节点的子树进行中序遍历
  2. 访问接节点
  3. 对根节点的子树进行中序遍历

遍历顺序如图

还是使用binaryTree这个树
递归版实现:

const inorder = root => {
  if(!root) return 
  inorder(root.left)
  console.log(root.val)
  inorder(root.right)
}
inorder(binaryTree)

非递归版实现:

const inorder = root => {
  if (!root) return;
  const stack = [];
  let p = root;
  while (stack.length || p) {
    while (p) {
      stack.push(p);
      p = p.left;
    }
    const n = stack.pop();
    console.log(n.val);
    p = n.right;
  }
}
inorder(binaryTree)

打印结果:

后序遍历算法

  1. 对根节点的子树进行中序遍历
  2. 对根节点的子树进行中序遍历
  3. 访问接节点

还是使用binaryTree这个树
递归版实现:

const postorder = (root) => {
  if (!root) return;
  postorder(root.left);
  postorder(root.right);
  console.log(root.val);
};
postorder(binaryTree);

非递归版实现:

const inorder = root => {
  if (!root) return;
  const outputStack = [];
  const stack = [root];
  while (stack.length) {
    const n = stack.pop();
    outputStack.push(n);
    if (n.left) stack.push(n.left);
    if (n.right) stack.push(n.right);
  }
  while (outputStack.length) {
    const n = outputStack.pop();
    console.log(n.val);
  }
}
inorder(binaryTree)

打印结果:

前端与树

遍历JSON的所有节点值

使用深度优先遍历

const json = {
  a: { b: { c: 1 } },
  d: [1, 2],
};
// 深度优先遍历
const dfs = (n, path) => {
  console.log(n, path);
  Object.keys(n).forEach((k) => {
    dfs(n[k], path.concat(k));
  });
};
dfs(json, []);

打印结果

图是什么

  • 图是网络结构的抽象模型, 是一组由边连接的节点
  • 图可以表示任何二元关系, 比如路,航班
  • js没有图, 可以用Array Object模拟

图的表示法

邻接矩阵

邻接表

图的遍历

  • 深度优先遍历: 尽可能深的搜索图的分支
  1. 访问根节点
  2. 对根节点的没访问过得相邻节点挨个进行深度优先遍历


定义一个图

const graph = {
  0:[1,2],
  1:[2],
  2:[0,3],
  3:[3]
}

使用深度优先遍历


const visited = new Set()
const dfs = n => {
  console.log(n)
  visited.add(n)
  graph[n].forEach(c => {
    if(!visited.has(c)) {
      dfs(c)
    }
  })
}
dfs(2)

打印结果

  • 广度优先遍历: 先访问离根节点最近的节点
  1. 新建一个队列, 把根节点入队
  2. 把队头出队并访问
  3. 把队头的没访问过得相邻节点入队
  4. 重复第二 三步, 直到队列为空

const bfs = node => {
  const visited = new Set()
  visited.add(node)
  const q = [node]
  while (q.length) {
    const n = q.shift()
    console.log(n)
    graph[n].forEach(c => {
      if(!visited.has(c)) {
        q.以上是关于[入门必看]数据结构3.2:队列的主要内容,如果未能解决你的问题,请参考以下文章

入门Java必须要掌握的基本数据类型,小白必看!

java变量与基本数据类型,易分辨,新手入门必看

java变量与基本数据类型,易分辨,新手入门必看

想转行当程序员的必看!java基础入门清华大学出版社

JavaEE入门级别最全教程4--初学者必看

前端必看js数据结构与算法(队列,链表,集合,字典,树,图,堆)