js数据结构与算法学习栈 队列 单链表
Posted lin-fighting
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了js数据结构与算法学习栈 队列 单链表相关的知识,希望对你有一定的参考价值。
认识数据结构
数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。 --百度百科
我们以自己的角度认识数据结构。
- 数据结构是计算机存储、组织数据的方式,计算机中数据量非常庞大,如何以高效的方式组织和存储呢?举个简单的例子,有一个图书馆,如何排放书本才能使书本找起来非常方便,而且有新书本的时候也能快速的放入到准确的位置。解决问题的方法,跟书本(数据)的组织方式有关,以什么样的方式来存储和组织我们的数据才能在使用数据的时候更加方便呢?这就i是数据结构需要考虑的问题。
常见的数据结构
常见的数据结构有,队列(queue),树,堆(heap),栈(stack),数组,链表,图,散列表等等。
每一种都有对应的应用场景,不同的数据结构的不同操作性能是不同的,有的查询很快,有的插入很快等等。
算法的定义
- 一个有限指令集,每条指令的描述不依赖于语言。
- 接受一些输入(有些情况不需要)
- 产生输出
- 一定在有限步骤后终止。
- 通俗的理解就是解决问题的办法/步骤逻辑。比如灯泡打不开的解决办法。
数组
常见语言的数组容量不会自动改变,需要扩容(重新申请内存,将原本的数组放入新的数组中),进行中间插入和删除操作的性能都比较低。
js对数组的封装已经非常完善。
栈(stack)
数组可以在任务位置插入和删除数据。而栈和队列是比较常见的受限的线性结构。
栈的特点就是先进后出,后进先出,可以把其理解成一个桶,只有一个入口,他们先进入的到最后肯定也是最后出来的。
添加元素只能在栈顶添加。
比如函数调用栈。
A调用B,B调用C。
那么在执行的过程中,A会先进入栈,A中调用了B,所以B也进栈,而B中又调用了C,所以C也进栈。最后C执行完毕后先出栈,再B出栈,再A出栈。
函数递归很容易引起栈溢出,就是因为自身不断调用自身,比如A递归调用A,会不断进行A进栈操作,次数一多就导致栈溢出。
栈结构的实现
基于数组实现。
实现栈的常见操作:
class Stack {
constructor() {
//存放所有栈中的元素
this.items = []
}
//栈相关操作
//进栈
push(item) {
this.items.push(item)
}
//出栈
pop() {
this.items.pop()
}
//获取最后一位元素
peek() {
return this.items[this.items.length - 1]
}
//判空
isEmpty() {
return !this.items.length
}
//个数
size() {
return this.items.length
}
//字符串i形式
toString() {
return this.items.toString()
}
}
//栈的使用
let s = new Stack()
s.push(12)
s.push(32)
console.log('当前的栈结构', s.items);
s.pop()
console.log('当前的栈结构', s.items);
console.log('最后一位元素', s.peek());
console.log('是否为空', s.isEmpty());
console.log('个数:', s.size());
console.log('字符串形式:', s.toString());
栈的实现还是相对简单的。
栈的应用
十进制转2进制。
算法就是将该十进制的数除以2,所有余数,从后往前拼起来。如
十进制10转成2进制就是
10/2 = 5…0
5/2 = 2…1
2/2 = 1…0
1/2 = 0…1
所以10的二进制就是1010。
我们可以依次将余数压入栈底,到最后再取出来。
结果正确。
队列结构
队列,另一种受限的线性表,特点是:先进先出(FIFO,first In first Out)。他只允许在表的前端进行删除操作,在表的后端进行插入操作。
生活中:排队上厕所。。
开发中:打印队列。线程队列。
js实现队列
- 基于数组
- 基于链表(后续学链表在实现)
基于数组
基于数组的队列性能不会太高,因为队列是先进先出,一旦删除首个元素之后,数组的其他元素都会往前偏移一位。
//队列常见操作:
// enqueue(element) 向队尾添加一个或多个项
// dequeue() 移除队列的第一项,并返回被移除的元素
// front() 返回队列中第一个元素。
// isEmpty()判空
// size() 个数
// toString 将队列中的内容转为字符串形式。
//封装队列类
class Queue {
constructor(){
this.items = []
}
enqueue(...element){
this.items.push(...element)
}
dequeue(){
this.items.shift()
}
front(){
return this.items[0]
}
isEmpty(){
return !this.items.length
}
size(){
return this.items.length
}
toString(){
return this.items.join('')
}
}
如上,封装完成。接着实现一个简单案例:击鼓传花
规则:有七个人,围成一排,数到五的人淘汰,问最后胜利的人,一开始的位置在哪里?
思路:使用队列,将七个人放进去队列,再依次出队列,如第一个出队列的是1,不是5,就将其放入队尾,依次类推,到5的时候就出队淘汰,剩一个的时候就知道他是谁了。
封装一个基于队列的函数,参数是七个参赛选手和数字,结果是胜利者
代码:
function Game(arr, num) {
const queue = new Queue()
//进队
queue.enqueue(...arr)
//定义变量
let i = 1;
//当队列长度大于1的时候执行
while (queue.size() > 1) {
if (i !== num) {
// 不是第五,就要加到最后,
queue.enqueue(queue.front())
i++
} else {
//i置为0,重新开始
i = 0
}
//执行一遍肯定会去掉一个
queue.dequeue()
}
//返回剩下的那个
console.log('胜利者是:', queue.front());
return arr.indexOf(queue.front()) + 1
}
console.log(Game(['lili', 'cc', 'bb', 'dd', 'ee', 'gg', 'hh'], 5));
答案是第二位的cc。
优先级队列
跟队列类似,但多了优先级的概念,比如飞机登机,头等舱商务舱的优先级就高于经济舱,在计算机中,每个线程处理的任务优先级不同,我们可以根据不同的任务优先级对其进行排序。
实现
我们先创建每一个元素的类。;
在插入的时候创建元素,并且for循环判断优先级,插入数组。
优先级队列就实现成功了。
链表
链表和数组一样,可以用于存储一系列的元素,但是实现机制完全不同。
数组:
创建数组需要申请一段连续的内存空间,并且大小固定,当数组需要扩容的时候,一般是申请一个更大的数组,然后将数组中的元素复制过去。比较耗费性能。
在数组开头或者中间位置插入数据的成本很高,需要进行大量元素的位移。
链表
链表不同于数组,链表中的元素在内存中不是连续的。每个元素都有两部分内容:1 存储元素本身的节点 2 一个指向下一个元素的引用(指针)
- 优点:
内存不是连续的,可以充分利用计算机的内存,实现灵活的内存管理。
不必在创建时候就确定大小,并且大小可以无限制延伸。
插入和删除数据的时候,时间复杂度为o(1) - 缺点:
访问任何一个元素的时候,都需要从头遍历(数组可以直接通过索引获取)
无法通过下表访问元素,需要一个个访问。 - 最简单的链表例子就是火车,火车通过一个个指针将每个车厢连接在一起,每个车厢上又有自己的内容。
实现链表
class Node {
constructor(data) {
this.data = data
this.next = null
}
}
class LinkedList {
constructor() {
//头指针
this.head = null
//链表的长度
this.length = 0
}
append(data) {
const element = new Node(data)
if (!this.head) {
this.head = element
} else {
let current = this.head
//遍历找到最后的节点
while (current.next) {
current = current.next
}
//插入
current.next = element
}
this.length++
}
// //特定位置插入
insert(position, data) {
if (typeof position !== 'number' || position < 0 || position > this.length) {
return false
}
const element = new Node(data)
//插入首位
if (position === 0) {
element.next = this.head
this.head = element
} else {
//插入中间的一位
let current = this.head
let index = 1
//采用插入位置前一位进行操作,比如插入到第四个,就将element.next指向第三个的下一个,再将第三个的下一个重新向element
while (index++ < position) {
// 如position = 4, index = 4的时候, current指向第三个,因为执行了两遍
current = current.next
}
// 让element变成第四个
element.next = current.next
current.next = element
// for (let i = 1; i < position; i++) {
// if (i === position - 1) {
// element.next = current.next
// current.next = element
// break;
// }
// current = current.next
// }
}
this.length++
}
// //获取对应位置的元素
get(position) {
if (typeof position !== 'number' || position < 0 || position >= this.length) {
return undefined
}
let current = this.head
let index = 0
while (index++ < position) {
current = current.next
}
return current.data
}
// //返回元素在列表中的索引
indexOf(data) {
let current = this.head
let index = 0
while (current) {
if (current.data === data) {
return index
}
current = current.next
index++
}
return -1
}
// //修改某个位置的元素
update(position, data) {
if (typeof position !== 'number' || position < 0 || position >= this.length) {
return false
}
let current = this.head
let index = 0
while (index++ < position) {
current = current.next
}
current.data = data
}
// //从列表的特定位置移除一项
removeAt(position) {
if (typeof position !== 'number' || position < 0 || position >= this.length) {
return false
}
let current = this.head
let index = 0
// 找到应该删除的节点的前一个,比如删除2,此时的current 为1所指向的节点。
while (index++ < position - 1) {
current = current.next
}
let element = current.next.next //保存第三个节点
current.next.next = null //让第二个节点与第三个节点断掉关系
current.next = element // 第一个节点指向第三个节点
this.length--
return true
}
// 从列表移除一项
remove(element) {
let current = this.head
let nextElement;
let isSuccess;
//如果第一个就是
if (current.data === element) {
nextElement = this.head.next
this.head = element
element.next = nextElement
return true
}
// 找到应该删除的节点的前一个,比如删除2,此时的current 为1所指向的节点。
while (current) {
if (current.next.data === element) {
nextElement = current.next.next //存粗下下个节点
current.next.next = null //断开下个节点与下下个节点的联系
current.next = nextElement //连接下下个节点
isSuccess = true
break;
} else {
current = current.next
}
}
//是否删除成功
if (isSuccess) {
this.length--
return true
}
return false
}
isEmpty() {
return !!this.length
}
size() {
return this.length
}
toString() {
let current = this.head
let str = ''
while (current) {
str += `,${current.data.toString()}`
current = current.next
}
return str.slice(1)
}
}
实现链表主要难的是几个方法,特定位置插入,特定位置删除等等。其思路就是借助循环从头到尾遍历找到对应的节点做对应的事情。
如特定位置插入,就需要找到特定位置前一个节点,将他的下个节点保存起来,然后将他的next指针指向插入的元素,再将插入元素的next指针指向下一个节点。这样就实现了。
以上是关于js数据结构与算法学习栈 队列 单链表的主要内容,如果未能解决你的问题,请参考以下文章
Java算法 -- 单链表的反转单链表实现栈和队列以及双端队列K 个一组翻转链表
Java算法 -- 单链表的反转单链表实现栈和队列以及双端队列K 个一组翻转链表
Java算法 -- 单链表的反转单链表实现栈和队列以及双端队列K 个一组翻转链表