数据结构(二)---栈、队列、集合、字典

一、栈

数组是一个线性结构,并且可以在数组的任意位置插入和删除元素。而栈和队列就是比较常见的受限的线性结构。
栈的特点:先进后出,后进先出(LIFO:last in first out)。
在这里插入图片描述
程序中的栈结构:

  • 函数调用栈:A(B(C(D()))):即A函数中调用B,B调用C,C调用D;在A执行的过程中会将A压入栈,随后B执行时B也被压入栈,函数C和D执行时也会被压入栈。所以当前栈的顺序为:A->B->C->D(栈顶);函数D执行完之后,会弹出栈被释放,弹出栈的顺序为D->C->B->A;
  • 递归:为什么没有停止条件的递归会造成栈溢出?比如函数A为递归函数,不断地调用自己(因为函数还没有执行完,不会把函数弹出栈),不停地把相同的函数A压入栈,最后造成栈溢出(Stack Overfloat)

栈常见的操作:

  • push(element):添加一个新元素到栈顶位置;
  • pop():移除栈顶的元素,同时返回被移除的元素;
  • peek():返回栈顶的元素,不对栈做任何修改(该方法不会移除栈顶的元素,仅仅返回它);
  • isEmpty():如果栈里没有任何元素就返回true,否则返回false;
  • size():返回栈里的元素个数。这个方法和数组的length属性类似;
  • toString():将栈结构的内容以字符串的形式返回。
1.封装栈类
class Stack {
    constructor() {
        // 存放栈中的元素
        this.items = [];
    }
    //栈的相关操作
    //1.push():压栈操作,添加新元素到栈顶
    //在类中,方法写出来就是直接添加到该类的原型对象上
    push(element) {
        this.items.push(element);
    }
    //2.从栈中取出元素,pop在数组中是删除最后一个元素,返回该元素
    pop() {
        return this.items.pop();
    }
    //3.查看栈顶元素
    peek() {
        return this.items[this.items.length - 1];
    }
    //4.判断栈是否为空
    isEmpty() {
        return this.items.length === 0;
    }
    //5.获取栈中元素个数
    size() {
        return this.items.length;
    }
    //6.toString方法转字符串
    toString() {
        return this.items.join('-');
    }
}

测试:

let stack = new Stack();
stack.push('zqq');
stack.push(18);
stack.push('td');
console.log(stack);
stack.pop();
console.log(stack.peek()); //18
console.log(stack.isEmpty()); //false
console.log(stack.size());//2
console.log(stack.toString()); //zqq-18
2.栈结构的简单应用

用栈封装一个十进制转二进制的算法

//思路:除以2取余,余数依次入栈,然后依次取出栈顶元素
function dec2bin(decNumber) {
    //1.生成一个栈的实例
    let stack = new Stack();
    //2.循环取余入栈,不确定循环次数,用while
    while (decNumber > 0) {
        // 2.1.获取余数并放入栈中
        stack.push(decNumber % 2);
        // 2.2.获取整除后的结果作为下一次运算的数字(floor:向下取整)
        decNumber = Math.floor(decNumber / 2);
    }
    //3.去除栈顶元素,拼接字符串(任何类型和字符串相加都会变成字符串)
    let result = '';
    while(!stack.isEmpty()) {
        result += stack.pop();
    }
    return result;
}
console.log(dec2bin(1000));// 1111101000
console.log(dec2bin(100));// 1100100
console.log(dec2bin(10));// 1010
二、队列

队列是是一种受限的线性表,特点为先进先出(FIFO:first in first out)。

  • 受限之处在于它只允许在表的前端(front)进行删除操作;
  • 在表的后端(rear)进行插入操作;

在这里插入图片描述
队列的应用:

  • 打印队列:计算机打印多个文件的时候,需要排队打印;
  • 线程队列:当开启多线程时,当新开启的线程所需的资源不足时就先放入线程队列,等待CPU处理;

队列类的实现:
队列的实现和栈一样,有两种方案:

  • 基于数组实现;
  • 基于链表实现;

队列的常见操作:

  • enqueue(element):向队列尾部添加一个(或多个)新的项;
  • dequeue():移除队列的第一(即排在队列最前面的)项,并返回被移除的元素;
  • front():返回队列中的第一个元素——最先被添加,也将是最先被移除的元素。队列不做任何变动(不移除元素,只返回元素信息与Stack类的peek方法非常类似);
  • isEmpty():如果队列中不包含任何元素,返回true,否则返回false;
  • size():返回队列包含的元素个数,与数组的length属性类似;
    toString():将队列中的内容,转成字符串形式;
1.封装队列类
class Queue {
     constructor() {
         //属性
         this.items = [];
     }
     //方法
     //1.将元素添加到队列中
     enqueue(element) {
         this.items.push(element);
     }
     //2.从队列中删除前端元素,并返回该元素
     dequeue() {
         return this.items.shift();
     }
     //3.查看前端的元素
     front() {
         return this.items[0];
     }
     //4.查看队列是否为空
     isEmpty() {
         return this.items.length === 0;
     }
     //5.查看队列中元素的个数
     size() {
         return this.items.length;
     }
     //6.toString方法
     toString() {
         return this.items.join('');
     }
 }

测试:

 let queue = new Queue();
 queue.enqueue('abc');
 queue.enqueue(6);
 queue.enqueue(8);
 console.log(queue);
 queue.dequeue();  //删除abc
 console.log(queue);
 console.log(queue.front());  //6
 console.log(queue.isEmpty());  //false
 console.log(queue.size());  //2
 console.log(queue.toString());  //'68'
2. 队列的简单应用

使用队列实现小游戏:击鼓传花,传入一组数据和设定的数字num,循环遍历数组内元素,遍历到的元素为指定数字num时将该元素删除,直至数组剩下一个元素。

击鼓传花规则: 围成一圈数数,比如规定数到5的人淘汰,那么淘汰的人后面的人从1开始数,然后数到5的人再淘汰,以此类推,直到剩下最后一个人,求该人的位置。
思路: 围成一圈的话,可以看成一个队列,从第一个人开始数,那么在数到num之前的人可以依次从队头删除,加入队尾(保持环形结构)。直到被数到的这个人到队头,然后把它删除就行了。以上循环依次进行,直到队列只剩一个元素,返回该元素和它的位置。

//求最后剩下的那个人的位置,传入参数(名字列表,数字)
function passGame(nameList, num) {
    //1.定义一个队列,并依次把元素加入队列
    let queue = new Queue();
    for(let i = 0; i < nameList.length; i++) {
        queue.enqueue(nameList[i]);
    }
    //console.log('初始队列元素:',queue.items);
    //2.从第一个人开始数数
    while(queue.size() > 1) {
    	//队列中只剩1个人就停止数数
      	// 不是num的时候,重新加入队列末尾
     	// 是num的时候,将其从队列中删除
        //3.1依次把num之前的数字从队头取出,放到队尾
        for(let i = 0; i < num - 1; i++) {
            queue.enqueue(queue.dequeue());
        }
        //3.2删除第一个元素(数到num的人)
        //由于队列没有像数组一样的下标值不能直接取到某一元素,所以采用,把num前面的num-1个元素先删除后添加到队列末尾,这样第num个元素就排到了队列的最前面,可以直接使用dequeue方法进行删除
        queue.dequeue();
        //console.log(`人数变化:`,queue.items);
    }
    //4.把剩下那个人的名字和位置找出来
    let endName = queue.front();
    let index = nameList.indexOf(endName);
    console.log(`剩下的人是${endName},他的初始位置是:${index}`);
    return index;
}
//5.测试击鼓传花
let names = ['lily', 'lucy', 'Tom', 'Lilei', 'Tony']
console.log(passGame(names, 3));
// 剩下的人是LiLei,他的初始位置是3
3.封装优先级队列

所谓优先级队列,其实就是有人可以插队。那么封装的时候队列中元素应该是带着一个优先级标识,标识nb的元素就可以往前靠。也就是说需要有两个参数,分别是element(元素)和priority(优先级标识)。

(1)封装的逻辑:

a、首先判断是否为空,空就直接队尾插入;
b、不为空,就要遍历当前队列元素,插入的新值依次比较,如果新值优先级较低,那么就使用splice插入到当前位置的前面(把其他的顶到后面),同时跳出循环。
c、如果比较完之后新插入元素优先级最高,那么直接插入到队尾。

(2)代码实现
//优先队列内部的元素类
class QueueElement {
    constructor(element, priority) {
        this.element = element;
        this.priority = priority;
    }
}
//封装优先级队列
class PriorityQueue extends Queue {
    constructor() {
        super(); //super关键字调用父类的构造函数
    }
    //重写enqueue方法
    enqueue(element, priority) {
        //1.根据传入的元素,创建一个元素对象实例
        let queueElement = new QueueElement(element, priority);
        //2.1判断队列是否为空,如果为空那么直接push进去
        if (this.isEmpty()) {
            this.items.push(queueElement);
        } else {
            //2.2如果不为空,那么比较优先级(这里默认小的优先)
            let added = false;  //定义一个标识,判断是否已经插入
            for (let i = 0; i < this.items.length; i++) {
                //3.如果插入的元素优先级更小,那么就在当前位置插入(其他的被挤到后面)
                if (queueElement.priority < this.items[i].priority) {
                    this.items.splice(i, 0, queueElement);
                    added = true; //插入完成,修改标识
                    break; //如果插入完成,就跳出循环
                }
            }
            //4.如果遍历结束后,新元素始终最大,那么就添加到队尾
            if(!added) {
                this.items.push(queueElement);
            }
        }
    }
    //其他方法全部继承(转字符串需要小改)
    dequeue() {
        return super.dequeue(); //super关键字调用父类的函数
    }
    front() {
        return super.front();
    }
    isEmpty() {
        return super.isEmpty();
    }
    size() {
        return super.size();
    }
    toString() {
        let result = "";
        for(let item of this.items) {
            result += `${item.element}-${item.priority} `;
        }
        return result;
    }
}

测试:

let queue = new PriorityQueue();
queue.enqueue('zqq',10);  //参数1为元素,参数2为优先级
queue.enqueue('nba',50);
queue.enqueue('cba',20);
queue.enqueue('dg',40);
queue.enqueue('hy',30);
console.log(queue.items);
console.log(queue.toString()); //zqq-10 cba-20 hy-30 dg-40 nba-50 
(3)几个注意点

a、类的继承,super()直接调用相当于调用的是父类中的constructor函数,也就是说子类调用super相当于直接继承父类的属性(不调用super会报错)。如果要在子类中调用父类的方法,要使用super.方法名()
b、关于splice的用法,splice(位置,删除几个,替换的元素)

  • splice(1,0,‘Tom’):表示在索引为1的元素前面插入元素’Tom‘(也可以理解为从索引为1的元素开始删除,删除0个元素,再在索引为1的元素前面添加元素’Tom’);
  • splice(1,1,‘Tom’):表示从索引为1的元素开始删除(包括索引为1的元素),共删除1个元素,并添加元素’Tom’。即把索引为1的元素替换为元素’Tom’。
    c、数组的push方法在数组、栈和队列中的形式:
  • 数组: 在数组[0,1,2]中,pop(3),结果为[0,1,2,3];
  • 栈: 执行pop(0),pop(1),pop(2),pop(3),从栈底到栈顶的元素分别为:0,1,2,3;如果看成数组,可写为[0,1,2,3],但是索引为3的元素3其实是栈顶元素;所以说栈的push方法是向栈顶添加元素(但在数组的视角下为向数组尾部添加元素);
  • 队列: enqueue方法可以由数组的push方法实现,与数组相同,相当于在数组尾部添加元素。

可以这样理解:栈结构是头朝下(索引值由下往上增大)的数组结构。
在这里插入图片描述

三、集合

集合 比较常见的实现方式是哈希表,通常是由一组无序的、不能重复的元素构成。
数学中常指的集合中的元素是可以重复的,但是计算机中集合的元素不能重复。
集合是特殊的数组:

  • 特殊之处在于里面的元素没有顺序,也不能重复。
  • 没有顺序意味着不能通过下标值进行访问,不能重复意味着相同的对象在集合中只会存在一份。

实现集合类:

  • 在ES6中的Set类就是一个集合类,这里我们重新封装一个Set类,了解集合的底层实现。
  • JavaScript中的Object类中的key就是一个集合,可以使用它来封装集合类Set。
1.封装集合类
class Set {
    constructor() {
        //集合使用对象存储,因为元素不能重复(对象中属性不能重复)
        this.items = {};
    }
    //向集合添加一个新的项
    add(value) {
        //先判断有没有这个属性,有的话就不添加
        if(this.has(value)) return false;
        //使用[]的方式添加属性,这样属性名就和value一样
        this.items[value] = value;
        return true;
    }
    //检测集合(对象)中是否有这个属性
    has(value) {
        return this.items.hasOwnProperty(value);
    }
    //删除某个元素
    remove(value) {
        if(!this.has(value)) return false;
        delete this.items[value];
    }
    //移除集合中所有项
    clear() {
        this.items = {};
    }
    //返回集合的长度
    size() {
        return Object.keys(this.items).length;
    }
    //获取集合中所有的值
    getValues() {
        return Object.values(this.items);
    }
}
2.集合常见的操作
(1)并集

主要思路就是先创建一个新的集合,把集合1元素都放进去,然后遍历集合2,如果集合2中的元素没有在新集合中,就add进去。遍历集合2也可以直接将所有元素add进去,因为我们封装的add做了去重判断。

 union(otherSet) {
     //集合对象1:this
     //集合对象2:otherSet
     //1.创建一个新的集合
     let unionSet = new Set();
     //2.先把集合对象1中的元素全部放到新集合中
     let values = this.getValues();
     values.forEach(el => {
         unionSet.add(el);
     })
     //3.遍历集合对象2,判断其中的元素在新集合中有没有
     values = otherSet.getValues();
     for(let i = 0; i < values.length; i++) {
         //这里其实不写判断也可以,因为add方法做了判断
         if(!unionSet.has(values[i])) {
             unionSet.add(values[i]);
         }
     }
     //4.返回合并之后的结果
     return unionSet;
 }
(2)交集

主要思路就是遍历集合1,看看每个元素是否在集合2中存在,如果存在就添加到新的集合中,最后返回新的集合。

intersection(otherSet) {
    //1.定义一个交集集合
    let intersectionSet = new Set();
    //2.拿集合1中的元素和集合2中元素相比较
    let values1 = this.getValues();
    values1.forEach(el => {
        //集合1有集合2也有的收起来
        if(otherSet.has(el)) {
            intersectionSet.add(el);
        }
    })
    //3.返回交集集合
    return intersectionSet;
}
(3)差集

差集的思路和交集相反,遍历集合1,看看每个元素是否在集合2中存在,如果不存在就add到新集合里,最后返回新集合。

    //1.定义一个差集集合
    let diffSet = new Set();
    //2.集合1中的元素去集合2中查找
    let values1 = this.getValues();
    values1.forEach(el => {
        //集合1有集合2没有的收起来
        if(!otherSet.has(el)) {
            diffSet.add(el);
        }
    })
    //3.返回结果
    return diffSet;
}
(4)子集

主要思路是遍历集合1,看每个元素是否在集合2中存在,如果存在就return,不存在就继续查找,如果所有元素都在集合2中存在,那么久返回true。
这里需要注意在forEach中写return是无效的,只能跳出当前循环继续下一轮循环,并不能跳出整个循环,更不能跳出整个函数。所以这里要用for循环。

isSubset(otherSet) {
    //判断集合1是否是集合2的子集
    let values = this.getValues();
    //forEach的坑:return无法终止循环,无法跳出函数
    // values.forEach(el => {
    //     if(!otherSet.has(el)) return false;
    // }) 
    for(let i = 0; i < values.length; i++) {
        if(!otherSet.has(values[i])) return false;
    }
    return true;
}
四、字典

数组、集合、字典是任何语言都有的数据结构。其中集合和字典比较像,集合可以理解为只有值,字典可以理解为键值对。
需要注意,对象和ES6的Map数据结构是不一样的,对象的键可以是String可以是Symbol,而Map数据结构的键可以是任意数据类型。

1.字典的特点:
  • 字典存储的是键值对,主要特点是一一对应;
  • 比如保存一个人的信息:数组形式:[19,‘Tom’,1.65],可通过下标值取出信息;字典形式:{“age”:19,“name”:“Tom”,“height”:165},可以通过key取出value。
  • 此外,在字典中key是不能重复且无序的,而Value可以重复。

字典和映射的关系:

  • 有些编程语言中称这种映射关系字典,如Swift中的Dictonary,Python中的dict;
  • 有些编程语言中称这种映射关系Map,比如Java中的HashMap&TreeMap等;
2.封装字典类
//字典和集合差不多,都可以基于对象封装
class Map {
    constructor() {
        this.items = {};
    }
	//向字典中添加新元素
    add(key, value) {
        if(this.has(key)) return false;
        this.items[key] = value;
    }
	//判断字典中是否有某个key
    has(key) {
        return this.items.hasOwnProperty(key);
    }
	//从字典中移除元素
    remove(key) {
    	//1.判断字典中是否有这个key
        if(!this.has(key)) return false;
        //2.从字典中删除key
        delete this.items[key];
    }
	//根据key获取value
    get(key) {
        if(!this.has(key)) return undefined;
        return this.items[key]
    }
	//获取所有keys
    keys() {
        return Object.keys(this.items);
    }
    size() {
        return this.keys.length;
    }
    clear() {
        this.items = {};
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值