前言:不定时更新说明
本篇文章记录我自己在学习JS数据结构和算法过程中的一些学习笔记,将会不定时更新内容,有兴趣的小伙伴可以点个关注哟!!!
1. 栈(Stack)
实现栈结构有三种比较常见的方式:
1. 基于数组实现
2. 基于对象实现
3. 基于链表实现
1.1 基于数组实现栈
实现代码:
class Stack {
constructor(){
this.data = []
}
// 入栈
push(element){
this.data.push(element)
}
// 出栈
pop(){
return this.data.pop()
}
// 获取栈顶元素
peek(){
return this.data[this.data.length - 1]
}
// 判断栈是否为空
isEmpty(){
return this.data.length === 0
}
// 清空栈
clear(){
this.data = []
}
// 获取栈的长度
size(){
return this.data.length
}
}
简单测试:
let s = new Stack()
s.push(10)
s.push(50)
s.push(90)
s.push(40)
s.push(30)
console.log(s.data)
s.pop()
console.log(s.data)
s.pop()
console.log(s.data)
console.log('栈顶元素值:', s.peek())
console.log('栈是否为空:', s.isEmpty())
console.log('栈的长度:', s.size())
s.clear()
console.log(s)
测试截图:
注意:
创建一个Stack类最简单的方式是使用一个数组来存储其元素,但是在处理大量数据的时候,我们需要评估如何操作数据是最高效的。在使用数组时,大部分方法的时间复杂度是O(n),如果数组有很多元素的话,所需的时间会很长,另外,存储数组元素是内存开辟的一段连续的空间,会占用更多的空间资源。
1.2 基于对象实现栈
代码实现:
class Stack {
constructor(){
this.count = 0 // 记录栈的大小
this.data = {}
}
// 入栈
push(element){
this.data[this.count] = element
this.count++
}
// 判断栈是否为空
isEmpty(){
return this.count === 0
}
// 出栈
pop(){
if (this.isEmpty()) return undefined
this.count--
let popValue = this.data[this.count]
delete this.data[this.count]
return popValue
}
// 获取栈顶的值
peek(){
if (this.isEmpty()) return undefined
return this.data[this.count - 1]
}
// 获取栈的长度
size(){
return this.count
}
// 清空栈
clear(){
this.count = 0
this.data = {}
}
// 获取栈的内容,创建toString方法
toString(){
if(this.isEmpty()) return ''
let str = `${this.data[0]}`
for (let i = 1; i < this.count; i++){
str = `${str}, ${this.data[i]}`
}
return str
}
}
简单测试
let s = new Stack()
s.push(10)
s.push(50)
s.push(90)
s.push(40)
s.push(30)
console.log(s.toString())
s.pop()
console.log(s.toString())
s.pop()
console.log(s.toString())
console.log('栈顶元素值:', s.peek())
console.log('栈是否为空:', s.isEmpty())
console.log('栈的长度:', s.size())
s.clear()
console.log(s.toString())
测试截图
注意:
除了toString方法,我们创建的其他方法的时间复杂度均为O(1)
1.3 基于链表实现栈
首先我们先实现链表类:
//节点类
class Node {
constructor(data) {
this.data = data;
this.next = null;
}
}
// 单链表类
class LinkedList {
constructor() {
this.head = null;
this.length = 0;
this.next = null;
}
// 判断链表是否为空
isEmpty() {
return this.length === 0;
}
// 向链表尾部添加一个元素
push(data) {
const newNode = new Node(data);
if (this.isEmpty()) {
// 如果链表为空,将新节点赋给 head
this.head = newNode;
this.length++;
} else {
// 如果链表不为空,找到最后一个节点,将最后一个节点的 next 指向新节点,链表长度加一
let curNode = this.head;
while (curNode.next) {
curNode = curNode.next;
}
curNode.next = newNode;
this.length++;
}
}
// 返回索引位置的节点
getNodeAt(index) {
if (index >= 0 && index < this.length) {
let curNode = this.head;
if (index === 0) {
// 返回头部节点
return curNode;
}
for (let i = 0; i < index; i++) {
curNode = curNode.next;
}
return curNode;
}
return undefined;
}
// 从链表中移除元素
removeAt(index) {
if (index >= 0 && index < this.length) {
let curNode = this.head;
if (index === 0) {
// 如果移除第一个节点,即将 head 代表的节点移除
let cur = curNode;
this.head = curNode.next;
this.length--;
return cur;
} else {
// 移除除头部以外的一个节点
let preNode = this.getNodeAt(index - 1);
curNode = preNode.next;
preNode.next = curNode.next;
this.length--;
return curNode.data;
}
}
return undefined;
}
// 在链表特定位置插入元素
insert(index, data) {
if (index >= 0 && index <= this.length) {
const newNode = new Node(data);
if (index === 0) {
// 添加到头部
newNode.next = this.head;
this.head = newNode;
} else {
// 添加到除头部以外的位置
let preNode = this.getNodeAt(index - 1);
let curNode = preNode.next;
newNode.next = curNode;
preNode.next = newNode;
}
this.length++;
return true;
}
return false;
}
// 判断节点的 data 是否相等
isEqual(data1, data2) {
return data1 === data2;
}
// 返回一个给定节点的位置
indexOf(data) {
if (this.isEmpty()) return -1;
let curNode = this.head;
let index = 0;
while (curNode) {
if (this.isEqual(data, curNode.data)) return index;
index++;
curNode = curNode.next;
}
return -1;
}
// 从链表中移除指定 data 的节点
remove(data) {
let index = this.indexOf(data);
return this.removeAt(index);
}
// 返回头部节点
getHead() {
return this.head;
}
// 清空链表
clear() {
this.head = null;
this.length = 0;
this.next = null;
}
// 返回链表数据内容
toString() {
let curNode = this.head;
let str = '';
for (let i = 0; i < this.length; i++) {
str += curNode.data + ' ➡ ';
curNode = curNode.next;
}
str = str.substring(0, str.length - ' ➡ '.length);
return str;
}
}
然后再用链表实现栈结构:
class Stack {
constructor() {
this.items = new LinkedList();
}
// 入栈,将新元素添加到链表的头部
push(element) {
this.items.insert(0, element);
}
// 出栈,从链表头部移除元素
pop() {
return this.items.removeAt(0);
}
// 获取栈顶元素
peek() {
return this.items.getHead().data;
}
// 获取栈长度
size() {
return this.items.length;
}
// 判断栈是否为空
isEmpty() {
return this.items.length === 0;
}
// 清空栈
clear() {
this.items.clear();
}
// 获取栈内元素
toString() {
return this.items.toString();
}
}
简单测试一下:
// 测试
let stack = new Stack();
stack.push('崔堂袁');
stack.push('蒋天祥');
stack.push('王志鑫');
stack.push('周运来');
stack.push('尹广晗');
stack.push('詹锐');
console.log(stack.toString());
console.log('栈的长度:', stack.size());
stack.pop();console.log('================');
stack.pop();
stack.pop();
console.log(stack.toString());
console.log('栈的长度:', stack.size());
console.log('栈是否为空: ', stack.isEmpty());
console.log('栈顶元素: ', stack.peek());
stack.clear();console.log('================');
console.log('栈的长度:', stack.size());
console.log('栈是否为空: ', stack.isEmpty());
测试结果:
1.4 栈的简单应用
1.4.1 字符串中的括号匹配问题
题目: 现有一字符串仅由 ‘(’,‘)’,‘{’,‘}’,‘[’, ‘]’ 六种括号组成,若字符串满足下列条件之一,则为无效字符串:
① 任意类型的左右括号数量不相等;
② 存在未按正确顺序(先左后右)闭合的括号。
要求:输出括号的最大嵌套深度,若字符串无效则输出0。
示例:
给定字符串 “5*(3+6)-[5-42+(1-5)]",输出 3;
给定字符串 "5(3+6)-[5-4*2+(1-5))”,输出 0,因为满足无效字符串条件。
思路: 采用栈的思路来解决这个问题。
算法步骤:
① 定义一个空栈、一个记录栈最大长度的变量 maxLength = 0。
② 遍历字符串,若匹配到左边括号,将括号入栈,maxLength = stack.size() > maxLength ? stack.size() : maxLength,继续执行②;若匹配到右边括号,执行③;
③ 判断栈顶元素值是否与该括号匹配,若匹配,栈顶元素出栈,继续遍历,执行②;若不匹配,字符串无效,输出0,结束遍历
④ 遍历完成,判断栈是否为空,若为空,输出 maxLength(即为括号最大嵌套深度);若不为空,则字符串无效,输出0,结束。
关键代码:
<script>
class Stack {
constructor(){
this.data = []
}
push(element){
this.data.push(element)
}
pop(){
return this.data.pop()
}
peek(){
return this.data[this.data.length - 1]
}
isEmpty(){
return this.data.length === 0
}
size(){
return this.data.length
}
}
let str = '5*(3+6)-[5-4*2+(1-5)]'
let stack = new Stack()
let maxLength = 0
for(let s of str){
if (s === '{' || s === '[' || s === '(') {
stack.push(s)
maxLength = stack.size() > maxLength ? stack.size() : maxLength
}
if (s === '}' || s === ']' || s === ')') {
if (
stack.peek() === '{' && s === '}' ||
stack.peek() === '[' && s === ']' ||
stack.peek() === '(' && s === ')'
) {
stack.pop()
} else {
stack.push(s)
break
}
}
}
if (stack.isEmpty()) console.log('括号最大嵌套深度:', maxLength)
else console.log(0)
</script>
1.4.2 十进制转二进制
代码实现:
<script>
class Stack {
constructor(){
this.data = []
}
push(element){
this.data.push(element)
}
pop(){
return this.data.pop()
}
isEmpty(){
return this.data.length === 0
}
}
const baseConverter = (decNumber, base) => {
let stack = new Stack()
let rem = 0
let number = decNumber
let resultString = ''
while(number > 0) {
stack.push(number % base)
number = Math.floor(number / base)
}
while(!stack.isEmpty()){
resultString += stack.pop()
}
return resultString
}
console.log(baseConverter(100, 2))
</script>
2. 队列(Queue)
实现队列结构同栈一样有三种比较常见的方式:
1. 基于数组实现
2. 基于对象实现
3. 基于链表实现
2.1 基于数组实现队列
实现代码:
class Queue {
constructor(){
this.items = []
}
// 在队尾新加元素
enqueue(element){
this.items.push(element)
}
// 移除队首元素,并返回被移除的元素
dequeue(){
return this.items.shift()
}
// 获取队列最前面的元素
peek(){
return this.items[0]
}
// 队列是否为空
isEmpty(){
return this.items.length === 0
}
// 获取队列长度
size(){
return this.items.length
}
// toString
toString(){
let resStr = ''
for (let i = 0; i < this.items.length; i++){
resStr += (this.items[i] + ' ')
}
return resStr
}
}
简单测试:
let queue = new Queue()
// 入队
queue.enqueue('cty')
queue.enqueue('jtx')
queue.enqueue('zyl')
queue.enqueue('zr')
queue.enqueue('wzx')
queue.enqueue('ygh')
console.log(queue.toString())
// 出队
queue.dequeue()
console.log(queue.toString())
queue.dequeue()
console.log(queue.toString())
queue.dequeue()
console.log(queue.toString())
// 获取队列最前面的元素
console.log('队列最前面的元素是: ', queue.peek())
// 判断队列是否为空
console.log('队列是否为空: ', queue.isEmpty())
// 获取队列长度
console.log('队列长度为: ', queue.size())
测试截图:
2.2 基于对象实现队列
编码实现:
class Queue {
constructor(){
this.count = 0 // 控制队列大小
this.lowestCount = 0 // 追踪第一个元素
this.items = {}
}
// 队尾添加元素
enqueue(element){
this.items[this.count] = element
this.count++
}
// 判断队列是否为空
isEmpty(){
return this.count - this.lowestCount === 0
}
// 移除队首元素,并返回移除的元素
dequeue(){
if(this.isEmpty()) return undefined
let result = this.items[this.lowestCount]
delete this.items[this.lowestCount]
this.lowestCount++
return result
}
// 获取队首元素
peek(){
if(this.isEmpty()) return undefined
return this.items[this.lowestCount]
}
// 获取队列长度
size(){
return this.count - this.lowestCount
}
// toString
toString(){
let resStr = ''
for(let i = this.lowestCount; i < this.count; i++){
resStr += this.items[i] + ' '
}
return resStr
}
}
简单测试:
let queue = new Queue()
// 入队
queue.enqueue('cty')
queue.enqueue('jtx')
queue.enqueue('zyl')
queue.enqueue('zr')
queue.enqueue('wzx')
queue.enqueue('ygh')
console.log(queue.toString())
// 出队
queue.dequeue()
console.log(queue.toString())
queue.dequeue()
console.log(queue.toString())
queue.dequeue()
console.log(queue.toString())
// 获取队列最前面的元素
console.log('队列最前面的元素是: ', queue.peek())
// 判断队列是否为空
console.log('队列是否为空: ', queue.isEmpty())
// 获取队列长度
console.log('队列长度为: ', queue.size())
测试截图:
注意:
使用对象的方式创建队列比使用数组的方式更好,在获取元素的时候以及操作队列的时候,使用对象的方式都使数据结构更加高效,时间复杂度更低。
2.3 基于链表实现队列
首先实现链表类,链表类同1.3节基于链表实现栈的内容一样,在这里就不 copy 啦。
这里只实现用链表类创建队列:
class Queue {
constructor() {
this.items = new LinkedList();
}
// 入队,将新元素添加到链表的头部
push(element) {
this.items.insert(0, element);
}
// 出队,从链尾部移除元素
pop() {
return this.items.removeAt(this.items.length - 1);
}
// 获取队首元素
peek() {
return this.items.getNodeAt(this.items.length - 1).data;
}
// 获取队列长度
size() {
return this.items.length;
}
// 判断队列是否为空
isEmpty() {
return this.items.length === 0;
}
// 清空队列
clear() {
this.items.clear();
}
// 获取队列内元素
toString() {
return this.items.toString();
}
}
简单测试:
// 测试
let queue = new Queue();
queue.push('崔堂袁');
queue.push('蒋天祥');
queue.push('王志鑫');
queue.push('周运来');
queue.push('尹广晗');
queue.push('詹锐');
console.log(queue.toString());
console.log('队列的长度:', queue.size());
console.log('================');
queue.pop();
queue.pop();
queue.pop();
console.log(queue.toString());
console.log('队列的长度:', queue.size());
console.log('队列是否为空: ', queue.isEmpty());
console.log('队首元素: ', queue.peek());
console.log('================');
queue.clear();
console.log('队列的长度:', queue.size());
console.log('队列是否为空: ', queue.isEmpty());
测试截图:
2.4 双端队列
双端队列是一种允许我们同时从队列前端和后端添加和移除元素的特殊队列。
双端队列在现实生活中的例子有电影院、餐厅中排队的队伍等,举个栗子,一个刚买了票的人如果只是还需要再问一些简单的信息,就可以直接回到队列的头部。另外,在队伍末尾的人如果赶时间,他可以直接离开队伍。
在计算机科学中,双端队列的一个常见应用就是存储一系列的撤销操作。每当一个用户在软件中进行了一个操作,该操作会被存在一个双端队列中(就像在一个栈里),当用户点击撤销按钮时,该操作会从双端队列中弹出,表示它从后面被移除了,在进行预先定义的一定数量操作后,最先进行的操作会从双端队列的前端移除。
由于双端队列同时遵循了先进先出和后进先出原则,可以说他是一种把队列和栈相结合的一种数据结构。
创建 Deque 类:
class Deque {
constructor() {
this.count = 0
this.lowerCount = 0
this.items = {}
}
// 判断双端队列是否为空
isEmpty() {
return this.count - this.lowerCount === 0
}
// 在双端队列前端添加元素
addFront(element) {
this.items[--this.lowerCount] = element
}
// 在双端队列后端添加元素
addBack(element) {
this.items[this.count++] = element
}
// 在双端队列前端移除一个元素
removeFront() {
if(this.isEmpty()) return undefined
let ele = this.items[this.lowerCount]
delete this.items[this.lowerCount++]
return ele
}
// 在双端队列后端移除一个元素
removeBack() {
if(this.isEmpty()) return undefined
let ele = this.items[--this.count]
delete this.items[this.count]
return ele
}
// 获取双端队列前端第一个元素
peekFront() {
if(this.isEmpty()) return undefined
return this.items[this.lowerCount]
}
// 获取双端队列后端第一个元素
peekBack() {
if(this.isEmpty()) return undefined
return this.items[this.count - 1]
}
// 清空双端队列
clear() {
this.count = 0
this.lowerCount = 0
this.items = {}
}
// 获取双端队列长度
size() {
return this.count - this.lowerCount
}
// toString
toString() {
let str = ''
for (let i = this.lowerCount; i < this.count; i++) {
str += this.items[i] + ' '
}
return str
}
}
简单测试:
let d = new Deque()
d.addBack('addBack1')
d.addBack('addBack2')
d.addBack('addBack3')
d.addBack('addBack4')
console.log(d.toString())
console.log('双端队列长度: ', d.size())
console.log('双端队列前端第一个元素: ', d.peekFront())
console.log('双端队列后端第一个元素: ', d.peekBack())
console.log('双端队列是否为空: ', d.isEmpty())
d.addFront('addFront1')
d.addFront('addFront2')
d.addFront('addFront3')
console.log(d.toString())
console.log('双端队列长度: ', d.size())
console.log('双端队列前端第一个元素: ', d.peekFront())
console.log('双端队列后端第一个元素: ', d.peekBack())
console.log('双端队列是否为空: ', d.isEmpty())
d.removeFront()
d.removeFront()
d.removeBack()
d.removeBack()
console.log(d.toString())
console.log('双端队列长度: ', d.size())
console.log('双端队列前端第一个元素: ', d.peekFront())
console.log('双端队列后端第一个元素: ', d.peekBack())
console.log('双端队列是否为空: ', d.isEmpty())
d.clear()
console.log(d.toString())
console.log('双端队列长度: ', d.size())
console.log('双端队列前端第一个元素: ', d.peekFront())
console.log('双端队列后端第一个元素: ', d.peekBack())
console.log('双端队列是否为空: ', d.isEmpty())
d.addFront('cty')
d.addFront('jtx')
d.addFront('wzx')
d.addFront('zyl')
d.addBack('zr')
d.addBack('ygh')
console.log(d.toString())
console.log('双端队列长度: ', d.size())
console.log('双端队列前端第一个元素: ', d.peekFront())
console.log('双端队列后端第一个元素: ', d.peekBack())
console.log('双端队列是否为空: ', d.isEmpty())
测试截图:
2.5 队列的简单应用
2.5.1循环队列—击鼓传花游戏
游戏规则: 在这个游戏里,孩子们围成一个圆圈,把花尽快地传递给旁边的人,谋一时刻传花停止,这时候花在谁手里谁就退出圆圈,结束游戏,重复这个过程,直到只剩一个孩子,这个孩子就是最后的赢家。
程序模拟:我们会得到一份名单,把名单里面的名字全部加入队列,给定一个数字,从队列开头移除一项,再将其添加到队列队尾,一旦达到了给定的传递次数,移除队首,不再将其加入队尾(即为淘汰),继续整个过程,直到队列里面只剩最后一个元素为止。
程序实现:
<script>
const game = (nameList, num) => {
// 实例化一个队列
let queue = new Queue()
// 将数组元素加入队列
for(let i = 0; i < nameList.length; i++){
queue.enqueue(nameList[i])
}
// 依次出队并将出队元素入队,达到num条件的元素出队之后不再入队,继续循环,直到只剩最后一个元素
while(queue.size() > 1){
for(let i = 1; i < num; i++){
queue.enqueue(queue.dequeue())
}
queue.dequeue()
}
return queue.toString()
}
let winner = game(['cty', 'jtx', 'zyl', 'zr', 'wzx', 'ygh'], 3)
console.log(winner)
</script>
2.5.2 双端队列—回文检查器
回文是正反都能读的单词、词组、数或一系列字符序列,例如 madam 或 racecar。有不同的算法可以检查一个词组或字符串是否为回文,最简单的方式是将字符串反向排列并检查它是否和原字符串相同,如果相同,那它就是一个回文。但是利用数据结构来解决这个问题的最简单的方法是使用双端队列。
下面的算法使用了一个双端队列来解决问题:
const palindromeChecker = (str) => {
// 判断字符串是否合理
if (
str === undefined ||
str === null ||
str !== null && str.length === 0
) return false
// 如果字符串长度为1, 则必为回文
if (str.length === 1) return true
// 实例化一个双端队列
let d = new Deque()
// 结果,是回文则为true,不是则为false
let isEqual = true
let frontChar = ''
let backChar = ''
// 将字符传加入双端队列
for (let s of str){
d.addBack(s)
}
// 依次取出队列前端和后端元素进行比较,若不相同则不是回文,结束
while(d.size() > 1 && isEqual){
frontChar = d.removeFront()
backChar = d.removeBack()
if (frontChar !== backChar) isEqual = false
}
return isEqual
}
let str = '123454221'
console.log(str, palindromeChecker(str))
str = 'ab123454321ba'
console.log(str, palindromeChecker(str))
3. 链表(LinkedList)
3.1 单链表的实现
实现代码:
//节点类
class Node {
constructor(data) {
this.data = data;
this.next = null;
}
}
// 单链表类
class LinkedList {
constructor() {
this.head = null;
this.length = 0;
}
// 判断链表是否为空
isEmpty() {
return this.length === 0;
}
// 向链表尾部添加一个元素
push(data) {
const newNode = new Node(data);
if (this.isEmpty()) {
// 如果链表为空,将新节点赋给 head
this.head = newNode;
this.length++;
} else {
// 如果链表不为空,找到最后一个节点,将最后一个节点的 next 指向新节点,链表长度加一
let curNode = this.head;
while (curNode.next) {
curNode = curNode.next;
}
curNode.next = newNode;
this.length++;
}
}
// 返回索引位置的节点
getNodeAt(index) {
if (index >= 0 && index < this.length) {
let curNode = this.head;
if (index === 0) {
// 返回头部节点
return curNode;
}
for (let i = 0; i < index; i++) {
curNode = curNode.next;
}
return curNode;
}
return undefined;
}
// 从链表中移除元素
removeAt(index) {
if (index >= 0 && index < this.length) {
let curNode = this.head;
if (index === 0) {
// 如果移除第一个节点,即将 head 代表的节点移除
let cur = curNode;
this.head = curNode.next;
this.length--;
return cur;
} else {
// 移除除头部以外的一个节点
let preNode = this.getNodeAt(index - 1);
curNode = preNode.next;
preNode.next = curNode.next;
this.length--;
return curNode.data;
}
}
return undefined;
}
// 在链表特定位置插入元素
insert(index, data) {
if (index >= 0 && index <= this.length) {
const newNode = new Node(data);
if (index === 0) {
// 添加到头部
newNode.next = this.head;
this.head = newNode;
} else {
// 添加到除头部以外的位置
let preNode = this.getNodeAt(index - 1);
let curNode = preNode.next;
newNode.next = curNode;
preNode.next = newNode;
}
this.length++;
return true;
}
return false;
}
// 判断节点的 data 是否相等
isEqual(data1, data2) {
return data1 === data2;
}
// 返回一个给定节点的位置
indexOf(data) {
if (this.isEmpty()) return -1;
let curNode = this.head;
let index = 0;
while (curNode) {
if (this.isEqual(data, curNode.data)) return index;
index++;
curNode = curNode.next;
}
return -1;
}
// 从链表中移除指定 data 的节点
remove(data) {
let index = this.indexOf(data);
return this.removeAt(index);
}
// 返回头部节点
getHead() {
return this.head;
}
// 清空链表
clear() {
this.head = null;
this.length = 0;
this.next = null;
}
// 返回链表数据内容
toString() {
let curNode = this.head;
let str = '';
for (let i = 0; i < this.length; i++) {
str += curNode.data + ' ➡ ';
curNode = curNode.next;
}
str = str.substring(0, str.length - ' ➡ '.length);
return str;
}
}
3.2 双向链表的实现
代码实现:
// 节点类
class Node{
constructor(data){
this.data = data
this.prev = null
this.next = null
}
}
// 双向链表类
class DoublyLinkedList{
constructor(){
this.head = null
this.tail = null
this.length = 0
}
// 在链表尾部添加节点
append(data){
let newNode = new Node(data)
if(this.length === 0){ // 如果链表为空
this.head = newNode
this.tail = newNode
} else { // 如果链表不为空
this.tail.next = newNode
newNode.prev = this.tail
this.tail = newNode
}
this.length++
}
// 在任意位置插入新的节点
insert(data, index){
//边界值判定
if(index >= 0 && index <= this.length){
let newNode = new Node(data)
let current = this.head
let previous = null
if(index === 0){ // 在第一个位置插入
if(this.length === 0){ // 如果链表为空
this.head = newNode
this.tail = newNode
} else { // 如果链表不为空
newNode.next = current
current.prev = newNode
this.head = newNode
}
} else if(index === this.length) { // 在链表最后插入
this.tail.next = newNode
newNode.prev = this.tail
this.tail = newNode
} else { // 在链表中间插入
for(let i = 0; i < index; i++){
current = current.next
}
previous = current.prev
previous.next = newNode
newNode.prev = previous
newNode.next = current
current.prev = newNode
}
this.length++
}
// 无效的 index
else {
return false
}
}
// 根据索引值获取节点值
getVauleByIndex(index){
// 有效index
if(index >= 0 && index < this.length){
let current = null
// 判断从前往后遍历还是从后往前遍历
if(index < this.length / 2){ // 从前往后遍历
current = this.head
for(let i = 0; i < index; i++){
current = current.next
}
} else{ // 从后往前遍历
current = this.tail
for(let i = this.length - 1; i > index; i--){
current = current.prev
}
}
return current.data
}
// 无效index
else return undefined
}
// 从任意位置移除节点
removeAt(index){
// 有效 index
if(index >= 0 && index < this.length){
let current = this.head
if(index === 0){ // 删除第一个节点
this.head = current.next
} else if(index === this.length - 1){ // 删除最后一个节点
this.tail = this.tail.prev
} else { // 删除中间的节点
for(let i = 0; i < index; i++){
current = current.next
}
current.prev.next = current.next
current.next.prev = current.prev
}
this.length--
return true
}
// 无效 index
else return false
}
// 判断指定节点是否存在与链表中
has(data){
if(this.length === 0) return false
let current = this.head
while(current){
if(current.data === data) return true
current = current.next
}
return false
}
// 根据元素值返回节点的索引
indexOf(data){
let index = 0
let current = this.head
while(current){
if(current.data === data) return index
index++
current = current.next
}
return -1
}
// 修改某个位置的元素值
update(index, data){
if(index >=0 && index < this.length){
let current = null
if(index < this.length / 2){
current = this.head
for(let i = 0; i < index; i++){
current = current.next
}
} else {
current = this.tail
for(let i = this.length - 1; i > index; i--){
current = current.prev
}
}
current.data = data
return true
} else return false
}
// 判断链表是否为空
isEmpty(){
return this.length === 0
}
// toString
toString(){
let current = this.head
let str = ''
while(current){
str = str + current.data + ' ←→ '
current = current.next
}
str = str.substring(0, str.length - ' ←→ '.length)
return str
}
// 获取链表长度
size(){
return this.length
}
}
简单测试:
let L = new DoublyLinkedList()
L.append('崔堂袁')
L.append('蒋天祥')
L.append('王志鑫')
L.append('周运来')
L.append('尹广晗')
L.append('詹锐')
console.log(L.toString())
L.insert('李世民', 3)
L.insert('朱瞻基', 4)
console.log(L.toString())
console.log('"王志鑫"在链表中的位置索引为:', L.indexOf('王志鑫'))
L.update(5, '周雨枫')
console.log(L.toString())
L.removeAt(3)
L.removeAt(3)
console.log(L.toString())
运行结果:
3.3 有序链表的实现
有序链表是指保持元素有序的链表结构,除了使用排序算法之外,我们还可以将元素插入到链表中的正确位置来保证链表的有序性。
实现代码:
// 有序链表(大到小的顺序)
// 节点类
class Node {
constructor(value) {
this.value = value
this.next = null
}
}
// 有序链表类
class SortLinkedList {
constructor() {
this.head = null
this.length = 0
this.LESS_THAN = -1
this.MORE_THAN = 1
}
// 比较元素的大小(也可能是根据对象属性值的大小比较)
compareFun(a, b) {
return a - b > 0 ? this.MORE_THAN : this.LESS_THAN
}
// 添加节点
insert(value) {
let newNode = new Node(value)
if (this.length === 0) {
// 链表为空
this.head = newNode
this.length++
} else {
// 链表不为空
if (this.compareFun(this.head.value, newNode.value) === this.LESS_THAN) {
// 第一个节点就小于value
newNode.next = this.head
this.head = newNode
this.length++
return
}
let current = this.head
let previous = null
while (current) {
if (this.compareFun(current.value, newNode.value) === this.LESS_THAN)
break
previous = current
current = current.next
}
previous.next = newNode
newNode.next = current
this.length++
}
}
// 返回链表数据内容
toString() {
let current = this.head
let str = ''
while (current) {
str += current.value + ' ➡ '
current = current.next
}
str = str.substring(0, str.length - ' ➡ '.length)
return str
}
}
简单测试:
let L = new SortLinkedList()
L.insert(4)
L.insert(7)
L.insert(3)
L.insert(5)
L.insert(9)
L.insert(4)
L.insert(9)
console.log(L.toString())
运行截图:
4. 集合(Set)
4.1 手撕代码实现集合
集合是由一组无序且唯一的项组成。
代码实现:
class Set {
constructor() {
this.items = {}
this.size = 0
}
// 判断集合中是否包含某个元素
has(element) {
return Object.prototype.hasOwnProperty.call(this.items, element)
/*
Object 原型有 hasOwnProperty 方法,该方法返回一个表明对象中是否具有特定属性的布尔值
in 方法(element in this.items)也能判断对象中是否存在某个属性,但是 in 运算符是返回对象原型链上是否具有特定属性的布尔值
此外,可以用 this.items.prototype.hasOwnProperty(element)。但是对象上面的 hasOwnProperty 方法可能被覆盖掉导致代码不能正常工作
*/
}
// 向集合中添加元素
add(element) {
if (this.has(element)) return false
this.items[element] = element
this.size++
}
// 从集合中删除元素
delete(element) {
if (this.has(element)) {
delete this.items[element]
this.size--
return true
}
return false
}
// 清空集合
clear() {
this.items = {}
this.size = 0
}
// 返回集合中元素个数
size() {
return this.size
}
// 返回集合元素组成的数组
values() {
let list = []
for (let key in this.items) {
if (this.has(key)) list.push(this.items[key])
}
return list
}
}
简单测试:
let set = new Set()
set.add('崔堂袁')
set.add('蒋天祥')
set.add('王志鑫')
set.add('王志鑫')
set.add('周运来')
set.add('尹广晗')
set.add('尹广晗')
set.add('詹锐')
console.log(set.values())
set.delete('尹广晗')
set.delete('王志鑫')
console.log(set.values())
运行截图:
4.2 集合运算
并集: 对于给定的两个集合,返回一个包含两个集合中所有元素的新集合。
交集: 对于给定的两个集合,返回一个包含两个集合中共用元素的新集合。
差集: 对于给定的两个集合,返回一个包含所有存在于第一个集合但不存在于第二个集合的元素的新集合。
子集: 验证一个给定集合中的所有元素是否都存在于另一集合中。
代码实现:
class Set {
...
...
...
// 并集
union(otherSet) {
let newSet = new Set()
this.values().forEach((value) => newSet.add(value))
otherSet.values().forEach((value) => newSet.add(value))
return newSet
}
// 交集
intersection(otherSet) {
let newSet = new Set()
this.values().forEach((value) => {
if (otherSet.has(value)) newSet.add(value)
})
return newSet
}
// 差集
difference(otherSet) {
let newSet = new Set()
this.values().forEach((value) => {
if (!otherSet.has(value)) newSet.add(value)
})
return newSet
}
// 子集
isSubsetOf(otherSet) {
if (this.size > otherSet.size()) return false
let isSubset = true
this.values().forEach((value) => {
if (!otherSet.has(value)) {
isSubset = false
return
}
})
return isSubset
}
}
简单测试:
let set1 = new Set()
let set2 = new Set()
set1.add(1)
set1.add(2)
set1.add(3)
set1.add(4)
set1.add(5)
console.log('set1: ', set1.values())
set2.add(3)
set2.add(4)
set2.add(5)
set2.add(6)
set2.add(7)
console.log('set2: ', set2.values())
console.log('并集: ', set1.union(set2).values())
console.log('交集: ', set1.intersection(set2).values())
console.log('差集: ', set1.difference(set2).values())
console.log('set1是否是set2的子集: ', set1.isSubsetOf(set2))
运行截图:
4.3 ES6 自带的集合类
ECMAScript 2015——Set 类
const set = new Set();
set.add(1);
console.log(set.values()); // 输出 @Iterator 迭代对象
console.log(set.has(1)); // 输出 true
console.log(set.size); // 输出 1
与我们自己写的 Set 不同的是,ES6 的 values 方法返回的是一个可迭代对象,而不是值构成的数组。delete 和 clear 方法跟我们实现的功能一样。
此外,ES6 的 Set 类支持向构造函数传入一个数组来初始化一个集合,
eg:const newSet = new Set([1, 2, 3, 3, 3, 4, 5, 6, 5]) // newSet = {1, 2, 3, 4, 5, 6}
ES6 Set 类的运算:
我们的类实现了并集、交集、差集和子集的运算,然而 ES6 的 Set 并没有这些功能,不过咱们也可以模拟不是?
const setA = new Set()
setA.add(1)
setA.add(2)
setA.add(3)
setA.add(4)
setA.add(5)
const setB = new Set()
setB.add(3)
setB.add(4)
setB.add(5)
setB.add(6)
setB.add(7)
// 模拟并集运算
const union = (setA, setB) => new Set([...setA, ...setB])
// 模拟交集运算
const intersection = (setA, setB) =>
new Set([...setA].filter((x) => setB.has(x)))
// 模拟差集运算
const difference = (setA, setB) =>
new Set([...setA].filter((x) => !setB.has(x)))
类型转换:
// Array 转 Set
const mySet = new Set(['value1', 'value2', 'value3'])
console.log(mySet) // Set(3) { 'value1', 'value2', 'value3' }
// 用...操作符,将 Set 转 Array
const myArray = [...mySet]
console.log(myArray) // [ 'value1', 'value2', 'value3' ]
// String 转 Set
const mySet1 = new Set('hello')
console.log(mySet1) // Set(4) { 'h', 'e', 'l', 'o' }
// 注:Set 中 toString 方法是不能将 Set 转换成 String
5. 字典(Map)
在字典中,存储的是键值对,其中键名用来查询元素, 任何值(对象或者原始值) 都可以作为一个键或一个值。
字典和集合类似,集合是以值值的形式存储元素,字典则是以键值对的形式来存储元素。
字典也叫映射、符号表或关联数组。
Map 与 Object 的区别:
① 一个 Object 的键只能是字符串或者 Symbol,但是一个 Map 的键可以是任意值。
② Map 中的键值是有序的(FIFO原则),而对象中的键值是无序的。
③ Map 中的键值对个数可以从 size 属性中获取,而 Object 中的键值对个数只能手动计算。
④ Object 都有自己的原型。原型链上的键名有可能和自己在对象上设置的键名产生冲突。
Map 中的 key:
① key 是字符串:
const myMap = new Map()
let strKey = "a string"
myMap.set(strKey, "cty")
myMap.get(strKey) // 'cty'
myMap.get("a string") // 'cty' 因为 keyString === 'a string'
② key 是对象:
const myMap = new Map()
var objKey = {}
myMap.set(objKey, 'cty')
myMap.get(objKey) // cty
myMap.get({}) // undefined
// 因为 keyObj !== {}, 第5行的{}是在底层重新new了一个对象
③ key 是函数:
const myMap = new Map()
const keyFunc = function () {}
myMap.set(keyFunc, 'cty')
myMap.get(keyFunc) // 'cty'
myMap.get(function () {}) // undefined, 因为 keyFunc !== function () {}
④ key 是 NaN:
const myMap = new Map()
myMap.set(NaN, 'not a number')
myMap.get(NaN) // "not a number"
let otherNaN = Number('foo')
myMap.get(otherNaN) // "not a number"
// 虽然 NaN 和任何值甚至和自己都不相等(NaN !== NaN 返回 true),但是NaN作为Map的键来说是没有区别的。
Map 的迭代:
① for … of … 迭代:
const myMap = new Map()
myMap.set('cty', 24)
myMap.set('jtx', 22)
myMap.set('wzx', 21)
for (let [key, value] of myMap) {
console.log(key, value)
}
/*
cty 24
jtx 22
wzx 21
*/
② forEach 迭代:
const myMap = new Map()
myMap.set('cty', 24)
myMap.set('jtx', 22)
myMap.set('wzx', 21)
myMap.forEach((value, key) => console.log(key, value))
/*
cty 24
jtx 22
wzx 21
*/
Map 自带的方法:
const myMap = new Map()
myMap.set('cty', 24)
myMap.set('jtx', 22)
myMap.set('wzx', 21)
// entries 方法返回一个新的 Iterator(可迭代)对象,它按插入顺序包含了 Map 对象中每个元素的 [key, value] 数组
console.log(myMap.entries())
// values 方法返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的值
console.log(myMap.values())
// keys 方法返回一个新的 Iterator 对象, 它按插入顺序包含了 Map 对象中每个元素的键
console.log(myMap.keys())
Map 对象的操作:
① Map 与 Array 的转换:
let arr = [
[1, '崔堂袁'],
[2, '蒋天祥']
]
// Map 构造函数可以将一个二维键值对数组转换成一个 Map 对象
const myMap = new Map(arr)
console.log(myMap) // Map(2) { 1 => '崔堂袁', 2 => '蒋天祥' }
// 使用 Array.from 函数可以将一个 Map 对象转换成一个二维键值对数组
let arr1 = Array.from(myMap)
console.log(arr1) // [ [ 1, '崔堂袁' ], [ 2, '蒋天祥' ] ]
② Map 的克隆:
let arr = [
[1, '崔堂袁'],
[2, '蒋天祥']
]
const map1 = new Map(arr)
const map2 = new Map(map1)
console.log(map1 === map2) // false, Map 对象构造函数生成实例,迭代出新的对象
③ Map 的合并:
const first = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three']
])
const second = new Map([
[1, 'uno'],
[2, 'dos']
])
// 合并两个 Map 对象时,如果有重复的键值,则后面的会覆盖前面的
const merged = new Map([...first, ...second])
console.log(merged) // Map(3) { 1 => 'uno', 2 => 'dos', 3 => 'three' }
6. 各种排序算法
描述:
n:数据规模
k:“桶”的个数
In-place:占用常数内存,不占用额外内存
Out-place:占用额外内存
稳定性:排序后两个相等键值的顺序和排序之前他们的顺序相同
6.1 冒泡排序
算法描述:
依次比较相邻两个元素,如果两个元素顺序不对则交换两个元素的位置
function bubbleSort(arr) {
let len = arr.length;
for (let i = 0; i < len - 1; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
arr[j] = arr[j] ^ arr[j + 1];
arr[j + 1] = arr[j] ^ arr[j + 1];
arr[j] = arr[j] ^ arr[j + 1];
}
}
}
return arr;
}
let arr = [4, 5, 1, 6, 2, 7, 2, 8];
arr = bubbleSort(arr);
console.log(arr); // [1, 2, 2, 4, 5, 6, 7, 8]
6.2 快速排序
算法描述: