数据结构与算法之队列(JavaScript)

一、什么是队列

队列是一种先进先出(FIFO)的有序结构,它在队尾添加新元素,在顶部移除元素。在生活中我们最常见的就是排队了,谁在前就先服务谁。在程序中也可以经常看到它的身影,比如JS的事件队列。所以,学习队列对我们理解生活和计算机十分有帮助。

二、实现队列

我们根据队列先进先出的特征,在JS中来模拟实现这个结构。我会创建一个Queue类,里面会包含以下方法:

  • enqueue:进入队列
  • dequeue:从队列移除,返回这个被移除的元素
  • peek:返回队列第一个元素
  • size:返回队列的长度
  • isEmpty:队列是否为空
  • clear:清空队列
  • toString:返回队列的字符串

接下来我会使用ES6的语法来实现它们。

实现Queue

首先创建一个Queue类,使用一个对象来模拟队列。在构造函数中,会有一个count属性和lowestCount属性,它们的作用分别是控制队列的长度以及追踪当前队列的第一个元素。

class Queue {
	constructor () {
		this.count = 0;
		this.lowestCount = 0;
		this.items = {};
	}
}

实现 enqueue方法

这个方法实现很简单,就是往队列的后一项增加一个元素,使用count作为下标。

enqueue (element) {
	this.items[this.count++] = element
}

实现 isEmpty方法

我们使用一个对象来实现的队列,就那么不能简单像数组那样直接返回this.count的值,因为这个下标是代表的一个元素,比如从前面移除了一个元素,我们需要让lowestCount往后移一位来跟踪第一个元素,后面的元素的序号是不能变的。如果当前lowestCountcount相等的话说明当前队列为空了。可能不是很容易理解,我画张图吧。

数据结构与算法之队列(JavaScript)_第1张图片

为了容易理解,我画一个类似数组的结构。这下我们可以很容易看到,只要this.lowest和this.count相等就表明队列已经空了(好好理解一下)。这下代码就出来了:

isEmpty () {
	return this.count - this.lowestCount === 0;
}

(我相信会有人看出来,其实我本人就是不容易理解所以画了图,嘿嘿。)

实现 size方法

有了上面的理解,这个实现就简单了。

size () {
	return this.count - this.lowestCount;
}

实现 dequeue方法

要删除一个元素需要先判断队列是否为空,有了上面的方法作铺垫就好说了。分三步走:

  1. 通过this.lowestCount返回当前第一个元素,并且存储到一个变量result
  2. 使用delete操作符来删除一个对象中的属性
  3. this.lowestCount往后一位,并且返回这个result
dequeue () {
	if (this.isEmpty()) {
		return undefiend;
	}
	const result = this.items[this.lowestCount];
	
	delete this.items[this.lowestCount];
	this.lowestCount++;
	
	return result;
}

实现 peek方法

这个方法是返回队列第一个元素,不用多说了吧,先判断为空,再利用this.lowestCount返回第一个元素。

peek () {
	if (this.isEmpty()) {
		return undefiend;
	}
	return this.items[this.lowestCount];
}

实现 clear方法

这个方法也很简单,令属性全部设为初始值就可以了。

clear () {
	this.count = 0;
	this.lowestCount = 0;
	this.items = {};
}

实现 toString方法

这个方法主要将队列每个元素转化为字符串,每个对象都有默认的toString方法,我们自己实现一下。实现方法其实也不是特别难,借助了递归思想。

toString() {
	if (this.isEmpty()) {
		return '';
	}
	let objString = `${this.items[this.lowestCount]}`;
	for (let i = this.lowestCount + 1; i > 0; i--) {
		objString = `${objString},${this.items[i]}`
	}
	return objString;
}

整个代码如下:

class Queue {
  constructor() {
    this.count = 0;
    this.lowestCount = 0;
    this.items = {};
  }

  enqueue(element) {
    this.items[this.count++] = element;
  }

  dequeue() {
    if (this.empty()) {
      return undefined;
    }

    const result = this.items[this.lowestCount];
    delete this.items[this.lowestCount];
    this.lowestCount++;
    return result;
  }

  peek() {
    if (this.empty()) {
      return undefined;
    }

    return this.items[this.lowestCount];
  }

  empty() {
    return this.count - this.lowestCount === 0;
  }

  size() {
    return this.count - this.lowestCount;
  }

  toString() {
    if (this.empty()) {
      return '';
    }

    let objString = `{this.items[this.lowestCount]}`;

    for (let i = this.lowestCount + 1; i < this.count; i++) {
      objString = `${objString},${this.items[i]}`;
    }

    return objString;
  }
}

三、双向队列

这种队列和上面的队列不一样,它遵循了队列和栈的的特征,既可以先进先出,又可以后进先出,是一种很特殊的结构。这对我们解决问题提供了极大的帮助。还是拿排队买票来说,排在第一位的可以先服务,最后一个如果不想排了,它也可以先离开。下面我们来实现这个双向队列。根据这个队列的特征,主要是以下方法:

  • addFont:从双向队列前端添加元素
  • addBack:从双向队列后端添加元素(和上面普通队列实现一致)
  • removeFont:从双向队列前端移除元素(和上面普通队列实现一致)
  • removeBack:从双向队列后端移除元素
  • peekFont:返回双向队列第一个元素(和上面普通队列实现一致)
  • peekBack:返回双向队列最后一个元素
  • isEmpty:当前双向队列是否为空(和上面普通队列实现一致)
  • size:双向队列的大小(和上面普通队列实现一致)
  • toString:返回双向队列的字符串(和上面普通队列实现一致)
  • clear:清空队列(和上面普通队列实现一致)

可以看出,大部分方法我们在上面的普通队列已经实现的,剩余的主要是跟栈的特性有关的方法了。
我们来实现这些剩余的方法吧。

实现 addFont方法

这个方法稍微有点复杂。我们有三种情况需要考虑:

  1. 队列为空,则实现的方法和addBack无疑。
  2. this.lowestCount大于0,直接添加在第一项即可
  3. this.lowestCount为0,则需要把队列整体往后移
addFont (element) {
	if (this.isEmpty()) {
		ths.addBack(element);
	} else if (this.lowestCount > 0) {
		this.items[--lowestCount] = element
	}  else {
		for (let i = count; i > 0; i--) {
			this.items[i] = this.items[i - 1];
		}		
        this.count++;
        this.lowestCount = 0;
        this.items[0] = element;
	}
}

实现 removeBack方法

这个跟栈的移除是一样的,实现很简单。

  removeBack() {
    if (this.isEmpty()) {
      return undefined;
    }
    const result = this.items[--this.count];
    delete this.items[this.count];
    return result;
  }

实现 peekBack方法

  peekBack() {
    if (this.isEmpty()) {
      return undefined;
    }

    return this.items[this.count - 1];
  }

整体代码如下:

class Deque {
  constructor() {
    this.count = 0;
    this.lowestCount = 0;
    this.items = {};
  }

  addFont(element) {
    // the queue is empty
    if(this.isEmpty()) {
      this.addBack(element)
    } else if (this.lowestCount > 0) {
      this.items[--this.lowestCount] = element;
    } else {
      for (let i = this.count; i > 0; i--) {
        this.items[i] = this.items[i - 1];
      }

      this.count++;
      this.lowestCount = 0;
      this.items[0] = element;
    }
  }

  addBack(element) {
    this.items[this.count++] = element;
  }

  removeFont() {
    if (this.isEmpty()) {
      return undefined;
    }
    const result = this.items[this.lowestCount];
    delete this.items[this.lowestCount];
    this.lowestCount++;
    return result;
  }

  removeBack() {
    if (this.isEmpty()) {
      return undefined;
    }
    const result = this.items[--this.count];
    delete this.items[this.count];
    return result;
  }

  peekFont() {
    if (this.isEmpty()) {
      return undefined;
    }

    return this.items[this.lowestCount];
  }

  peekBack() {
    if (this.isEmpty()) {
      return undefined;
    }

    return this.items[this.count - 1];
  }

  isEmpty() {
    return this.count - this.lowestCount === 0;
  }

  size() {
    return this.count - this.lowestCount;
  }

  clear() {
    this.count = 0;
    this.lowestCount = 0;
    this.items = {};
  }

  toString() {
    if (this.isEmpty()) {
      return '';
    }

    let objString = `${this.items[this.lowestCount]}`;

    for (let i = this.lowestCount + 1; i < this.count; i++) {
      objString = `${objString},${this.items[i]}`;
    }

    return objString;
  }
}

四、用队列解决问题

4.1 击鼓传花游戏

这个游戏相信大家都玩过,有可能不是击鼓,或许是挨个挨个传递某样东西(我们以前可能是随便乱扔,为了实现队列,就严谨一点吧),还有一个类似的游戏叫抢椅子,这个大家肯定玩过,就不多介绍了。在一定的时间后,东西在谁手里谁就会被淘汰,然后进入下一轮。直到只剩下一个人,那个人就是赢家。

我们可以用一个队列来把参与者放进去,然后循环队列,将第一个取出放在最后一个,时间一到,谁在第一个就被淘汰掉,然后接着游戏直到队列只有一个人为止。这个算法实现其实还是挺简单的,我们创建一个hotPotato函数,传递一个队列和一个模拟时间的数字。

function hotPotato(players, time) {
	// 1. 创建一个Queue实例
	const queue = new Queue()
	// 2. 淘汰者
	const eliminatedPlayers = []
	// 3. 将玩家加入队列
	players.forEach(player => {
		queue.enqueue(player)
	})
	// 4. 循环队列,直到只剩下一个人
	while (queue.size() > 1) {
		// 规定时间到,淘汰一个人
		for (let i = 0; i < time; i++) {
			queue.enqueue(queue.dequeue())
		}
		eliminatedPlayers.push(queue.dequeue())
	}
	// 5. 得到赢家
	let winner = queue.dequeue()
	// 6. 返回结果
	return {
		winner,
		eliminatedPlayers
	}
}

我们测试一下:

const players = ['老曹', '老刘', '老王', '老蔡', '老孔'];
const [, , time] = process.argv; // 获取用户输入
const { eliminatedPlayers, winner } = hotPotato(players, ~~time);

eliminatedPlayers.forEach(loser => {
  console.log(`${loser} has been eliminated in the hotPotato game`);
})

console.log(`the winner is ${winner}`);

在控制台输入并得到结果:
数据结构与算法之队列(JavaScript)_第2张图片
还是挺有意思的吧!下面我们看另一个问题:

4.2 回文检查器

回文是正反都能读通的字母、数字组成的字符串序列,如racecar、madam。

实现回文检查器有很多方法,但是这里我们借助了双向队列的思想来实现它,而且会十分简单,只需要将字符串放入一个双向队列中,并且不停取出第一个和最后一个元素比较,直到队列只剩下一个元素为止,如果期间有不同的就直接返回false,否则为true。我们用一个polindromeChecker函数来实现吧。

下面是思路:

  1. 检查字符串是否合法
  2. 去掉字符串特殊字符
  3. 实例化一个Deque对象
  4. 将字符串遍历到双向队列
  5. 取出队列第一项和最后一项比较,直到只剩下一个元素
function polindromeChecker(str) {
	if (typeof str === undefined || typeof str === null || (typeof str !== null && str.length === 0) {
		return false;
	}
	const deque = new Deque();
	const newString = str.toLocaleLowerCase().match(/[0-9a-z]+/g).join('');
	let isEqual = true;
	let firstName, lastName;
	
	for (let i = 0; i < newString.length; i++) {
		deque.addBack(newString.charAt(i))
	}

	while (deque.size() > 1) {
		firstName = deque.removeFont();
		lastName = deque.removeBack();
		if (firstName !== lastName) {
			isEqual = false;
		}
	}
	
	return isEqual;
}

加入测试用例:

console.log('a', panlindRomeChecker('a'));
console.log('aa', panlindRomeChecker('aa'));
console.log('kayak', panlindRomeChecker('kayak'));
console.log('Was it a car or a cat I saw ?', panlindRomeChecker('Was it a car or a cat I saw ?'));

数据结构与算法之队列(JavaScript)_第3张图片

全部都通过了,虽然在检查字符串那一块还有瑕疵,不过还是正常实现我们需要的回文检查器。
怎么样?是不是感觉使用队列解决问题是不是很简单。

在JS中,Event Loop 模型用到了事件队列,里面存放了各种事件的处理函数,等调用栈为空后然后执行队列的第一个函数。详细请看这篇文章,我个人觉得自从看了这篇 MDN的并发模型与事件循环文章后,你会对JS执行机制会有更多的领悟。之后我也会单独去写一篇文章来讲述这个概念。

五、总结

这篇文章算是大概写完了,看过《学习JavaScript数据结构与算法》(第3版)这本书的朋友应该会对这篇文章很熟悉,我是按照书上的结构写的文章,不过我是在看完书后合上书再来写的文章,代码也是一个一个打的,算是对知识的一个吸收吧,我的数据结构与算法上学时没怎么听过课,现在回头“还债”中。最后再提一下,一定要去看MDN的并发模型与事件循环文章,太棒了。

六、参考

【1】《学习JavaScript数据结构与算法》(第3版)
【2】并发模型与事件循环

你可能感兴趣的:(数据结构与算法)