IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> JavaScript 数据结构与算法(栈) -> 正文阅读

[JavaScript知识库]JavaScript 数据结构与算法(栈)

栈是一种遵从后进先出(LIFO)原则的有序集合。 新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。栈也被用在编程语言的编译器和内存中保存变量、方法调用等,也被用于浏览器历史记录(浏览器的返回按钮)

创建一个基于 JavaScript 数组的 Stack 类

创建一个类来表示栈,简单地从创建一个 stack-array.js 文件并声明 Stack 类开始

声明 Stack 类

class Stack {
    constructor() {
        this.items = []; // {1} 
    }
}

向栈添加元素

push(element(s)) 添加一个(或几个)新元素到栈顶。

push(element) {
    this.items.push(element);
}

从栈移除元素

pop() 移除栈顶的元素,同时返回被移除的元素。

pop() { 
    return this.items.pop(); 
   } 

查看栈顶元素

peek() 返回栈顶的元素,不对栈做任何修改(该方法不会移除栈顶的元素,仅仅返它。

peek() { 
    return this.items[this.items.length - 1]; 
   } 

检查栈是否为空

isEmpty() 如果栈里没有任何元素就返回 true,否则返回 false。

isEmpty() { 
    return this.items.length === 0; 
   }

清空栈元素

clear() 移除栈里的所有元素。

clear() { 
    this.items = []; 
   } 

实现栈的 length

size() 返回栈里的元素个数。该方法和数组lengt属性很类似。

size() { 
    return this.items.length; 
   }

完整的Stack类

class Stack {
    constructor() {
        this.items = []; // {1} 
    };
    push(element) {
        this.items.push(element);
    };
    pop() {
        return this.items.pop();
    };
    peek() {
        return this.items[this.items.length - 1];
    };
    isEmpty() {
        return this.items.length === 0;
    };
    clear() {
        this.items = [];
    };
    size() {
        return this.items.length;
    }
}

使用 Stack 类

首先需要初始化 Stack 类,然后验证一下栈是否为空

const stack = new Stack();
console.log(stack.isEmpty()); // 输出为 true 

往栈里添加一些元素(可以添加任意类型的元素)

stack.push(5);
stack.push(11);
stack.push(8);

如果调用 peek 方法,将输出 8,因为它是往栈里添加的最后一个元素。

console.log(stack.peek()); // 输出 8 
console.log(stack.size()); // 输出 3 
console.log(stack.isEmpty()); // 输出 false

使用基于数组的栈的问题

  • 在使用数组时,大部分方法的时间复杂度是 O(n)
  • 数组是元素的一个有序集合,为了保证元素排列有序,它会占用更多的内存空间。

创建一个基于 JavaScript 对象的 Stack 类

对于使用 JavaScript 语言实现栈数据结构的场景,也可以使用一个JavaScript 对象来存储所有的栈元素

首先像下面这样声明一个 Stack 类(stack.js 文件)。

class Stack {
    constructor() {
        this.count = 0;  //count 属性记录栈的大小
        this.items = {};
    }
    // 方法
    }

向栈中插入元素

在 JavaScript 中,对象是一系列键值对的集合。要向栈中添加元素,可以使用 count 变量作为 items 对象的键名,插入的元素则是它的值。在向栈插入元素后,递增 count 变量。

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

验证一个栈是否为空和它的大小

count 属性也表示栈的大小。因此,可以简单地返回 count 属性的值来实现 size 方法。

size() { 
 return this.count; 
} 

要验证栈是否为空,可以像下面这样判断 count 的值是否为 0。

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

从栈中弹出元素

由于我们没有使用数组来存储元素,需要手动实现移除元素的逻辑。pop 方法同样返回了从栈中移除的元素。

pop() { 
 if (this.isEmpty()) { // {1} 
 	return undefined; 
 } 
 this.count--; // {2} 
 const result = this.items[this.count]; // {3} 
 delete this.items[this.count]; // {4} 
 return result; // {5} 
} 

首先,需要检验栈是否为空(行{1})。如果为空,就返回 undefined。如果栈不为空的话,将 count 属性减 1(行{2}),并保存栈顶的值(行{3}),以便在删除它(行{4})之后将它返回(行{5})

查看栈顶的值并将栈清空

要访问栈顶元素,需要将 count 属性减 1。

peek() {
    if (this.isEmpty()) {
        return undefined;
    }
    return this.items[this.count - 1];
}

要清空该栈,只需要将它的值复原为构造函数中使用的值即可。

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

也可以遵循 LIFO 原则,使用下面的逻辑来移除栈中所有的元素。

while (!this.isEmpty()) {
    this.pop();
}

创建 toString 方法

在数组版本中,不需要关心 toString 方法的实现,因为数据结构可以直接使用数组已经提供的 toString 方法。对于使用对象的版本,需要创建一个 toString 方法来像数组一样打印出栈的内容。

toString() { 
 if (this.isEmpty()) { 
   return ''; 
 } 
 let objString = `${this.items[0]}`; // {1} 
 for (let i = 1; i < this.count; i++) { // {2} 
 objString = `${objString},${this.items[i]}`; // {3} 
 } 
 return objString; 
} 

如果栈是空的,只需返回一个空字符串即可。如果它不是空的,就需要用它底部的第一个元素作为字符串的初始值(行{1}),然后迭代整个栈的键(行{2}),一直到栈顶,添加一个逗号(,)以及下一个元素(行{3})。如果栈只包含一个元素,行{2}和行{3}的代码将不会执行。

实现了 toString 方法后,就完成了这个版本的 Stack 类。

class Stack {
    constructor() {
        this.count = 0;  //count 属性记录栈的大小
        this.items = {};
    }
    // 方法
    push(element) {
        this.items[this.count] = element;
        this.count++;
    };
    pop() {
        if (this.isEmpty()) { // {1} 
            return undefined;
        }
        this.count--; // {2} 
        const result = this.items[this.count]; // {3} 
        delete this.items[this.count]; // {4} 
        return result; // {5} 
    };
    peek() {
        if (this.isEmpty()) {
            return undefined;
        }
        return this.items[this.count - 1];
    };
    clear() {
        this.items = {};
        this.count = 0;
    };
    toString() {
        if (this.isEmpty()) {
            return '';
        }
        let objString = `${this.items[0]}`; // {1} 
        for (let i = 1; i < this.count; i++) { // {2} 
            objString = `${objString},${this.items[i]}`; // {3} 
        }
        return objString;
    }
}

这也是一个用不同方式写代码的例子,两者都提供了相同的功能,只是内部实现很不一样。

保护数据结构内部元素

在创建别的开发者也可以使用的数据结构或对象时,我们希望保护内部的元素,只有我们暴露出的方法才能修改内部结构。对于 Stack 类来说,要确保元素只会被添加到栈顶,而不是栈底或其他任意位置(比如栈的中间)。不幸的是,我们在 Stack 类中声明的 items 和 count 属性并没有得到保护,因为 JavaScript 的类就是这样工作的。

试着执行下面的代码。

const stack = new Stack(); 
console.log(Object.getOwnPropertyNames(stack)); // {1} 
console.log(Object.keys(stack)); // {2} 
console.log(stack.items); // {3} 

行{1}和行{2}的输出结果是[“count”, “items”]。这表示 count 和 items 属性是公开的,我们可以像行{3}那样直接访问它们。根据这种行为,我们可以对这两个属性赋新的值。

ES2015(ES6)语法创建了 Stack 类。ES2015 类是基于原型的。尽管基于原型的类能节省内存空间并在扩展方面优于基于函数的类,但这种方式不能声明私有属性(变量)或方法。另外,我们希望 Stack 类的用户只能访问我们在类中暴露的方法。

使用 JavaScript 来实现私有属性的方法。

下划线命名约定

一部分开发者喜欢在 JavaScript 中使用下划线命名约定来标记一个属性为私有属性。

class Stack { 
 constructor() { 
 this._count = 0; 
 this._items = {}; 
 } 

下划线命名约定就是在属性名称之前加上一个下划线(_)。不过这种方式只是一种约定,并不能保护数据,而且只能依赖于使用我们代码的开发者所具备的常识。

用 ES2015 的限定作用域 Symbol 实现类

ES2015 新增了一种叫作 Symbol 的基本类型,它是不可变的,可以用作对象的属性。看看怎么用它在 Stack 类中声明 items 属性(我们将使用数组来存储元素以简化代码)。

const _items = Symbol('stackItems'); // {1} 
class Stack { 
 constructor () { 
 this[_items] = []; // {2} 
 } 
  //栈的方法
}

在上面的代码中声明了 Symbol 类型的变量_items(行{1}),在类的 constructor 函数中初始化它的值(行{2})。要访问_items,只需要把所有的 this.items 都换成 this[_items]
这种方法创建了一个假的私有属性,因为 ES2015 新增的 Object.getOwnPropertySymbols方法能够取到类里面声明的所有 Symbols 属性。下面是一个破坏 Stack 类的例子。

const stack = new Stack();
stack.push(5);
stack.push(8);
let objectSymbols = Object.getOwnPropertySymbols(stack);
console.log(objectSymbols.length); // 输出 1 
console.log(objectSymbols); // [Symbol()] 
console.log(objectSymbols[0]); // Symbol() 
stack[objectSymbols[0]].push(1);
stack.print(); // 输出 5, 8, 1 

从以上代码可以看到,访问 stack[objectSymbols[0]] 是可以得到_items 的。并且,_items 属性是一个数组,可以进行任意的数组操作,比如从中间删除或添加元素(使用对象进行存储也是一样的)。但我们操作的是栈,不应该出现这种行为。

用 ES2015 的 WeakMap 实现类

有一种数据类型可以确保属性是私有的,这就是 WeakMap。WeakMap 可以存储键值对,其中键是对象,值可以是任意数据类型。如果用 WeakMap 来存储 items 属性(数组版本),Stack 类就是这样的:

const items = new WeakMap(); // {1} 
class Stack {
    constructor() {
        items.set(this, []); // {2} 
    }
    push(element) {
        const s = items.get(this); // {3} 
        s.push(element);
    }
    pop() {
        const s = items.get(this);
        const r = s.pop();
        return r;
    }
    // 其他方法
}

上面的代码片段解释如下。
行{ 1 } ,声明一个 WeakMap 类型的变量 items。
行{ 2 } ,在 constructor 中,以 this(Stack 类自己的引用)为键,把代表栈的数组存入 items。
行{ 3 } ,从 WeakMap 中取出值,即以 this 为键(行{ 2 } 设置的)从 items 中取值。

现在我们知道了,items 在 Stack 类里是真正的私有属性。采用这种方法,代码的可读性不强,而且在扩展该类时无法继承私有属性。鱼和熊掌不可兼得

用栈解决问题

如何解决十进制转二进制问题,以及任意进制转换的算法?

要把十进制转化成二进制,我们可以将该十进制数除以 2(二进制是满二进一)并对商取整,直到结果是 0 为止。

function decimalToBinary(decNumber) {
    const remStack = new Stack();
    let number = decNumber;
    let rem;
    let binaryString = '';
    while (number > 0) { // {1} 
        rem = Math.floor(number % 2); // {2} 
        remStack.push(rem); // {3} 
        number = Math.floor(number / 2); // {4} 
    }
    while (!remStack.isEmpty()) { // {5} 
        binaryString += remStack.pop().toString();
    }
    return binaryString;
}

在这段代码里,当除法的结果不为 0 时(行{1}),会获得一个余数,并放到栈里(行{2}、行{3})。然后让结果继续除以 2(行{4})。另外请注意:JavaScript 有数值类型,但是它不会区分整数和浮点数。因此,要使用 Math.floor函数仅返回除法运算结果的整数部分。最后,用 pop 方法把栈中的元素都移除,把出栈的元素连接成字符串(行{5})。

测试:

console.log(decimalToBinary(233)); // 11101001 
console.log(decimalToBinary(10)); // 1010 
console.log(decimalToBinary(1000)); // 1111101000 

进制转换算法

可以修改之前的算法,使之能把十进制转换成基数为 2~36 的任意进制。除了把十进制数除以 2 转成二进制数,还可以传入其他任意进制的基数为参数,就像下面的算法这样。

function baseConverter(decNumber, base) {
    const remStack = new Stack();
    const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // {6} 
    let number = decNumber;
    let rem;
    let baseString = '';
    if (!(base >= 2 && base <= 36)) {
        return '';
    }
    while (number > 0) {
        rem = Math.floor(number % base);
        remStack.push(rem);
        number = Math.floor(number / base);
    }
    while (!remStack.isEmpty()) {
        baseString += digits[remStack.pop()]; // {7} 
    }
    return baseString;
}

只需要改变一个地方。在将十进制转成二进制时,余数是 0 或 1;在将十进制转成八进制时,余数是 0~7;但是将十进制转成十六进制时,余数是 0~9 加上 A、B、C、D、E 和 F(对应 10、11、12、13、14 和 15)。因此,需要对栈中的数字做个转化才可以(行{6}和行{7})。因此,从十一进制开始,字母表中的每个字母将表示相应的基数。字母 A 代表基数 11,B 代表基数 12,以此类推。

测试

console.log(baseConverter(100345, 2)); // 11000011111111001 
console.log(baseConverter(100345, 8)); // 303771 
console.log(baseConverter(100345, 16)); // 187F9 
console.log(baseConverter(100345, 35)); // 2BW0
  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-07-25 16:13:42  更:2021-07-25 16:13:44 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/1 11:23:07-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码