boxmoe_header_banner_img

Hello! 欢迎来到盒子萌!

加载中

文章导读

01数据结构


avatar
Jack 2023年 4月 1日 218

JavaScript 数据结构

一、概述

数据结构是指相互之间存在一种或多种特定关系的数据组成的集合。采用合适的数据结构能给提高开发或者存储效率。比如我们学习Js时接触过的数据结构(Set、Map),在合适的时机使用它们能帮助我们更快的解决问题。

本阶段我们会学习自己创建几种数据结构,来帮助我们在日后的开发中解决特定的某类问题。

二、栈

1. 创建栈结构

是一种遵循后进先出(LIFO)原则的有序数据集。

栈顶、栈底、入栈、出栈 栈结构也被用在编译器和内存中保存变量等

下面我们来创建数据结构。功能需求:

添加数据(push)
返回栈顶数据(peek)
从栈中删除栈顶数据并返回(pop)
清空栈(clear)
返回栈中数据个数(size)
class Stack{
    constructor(){
        this.items = [];
    }
    push(...rest){
        rest.forEach(v=>{
            this.items.push(v);
        });
        return this.items.length;
    }
    peek(){
        return this.items[this.items.length-1];
    }
    pop(){
        return this.items.pop();
    }
    clear(){
        this.items = [];
    }
    size(){
        return this.items.length;
    }
}

检测是否满足栈的规则。

2. 栈结构的应用

  • 进制转换 (Js自带进制转换api,此案例并不实用,仅作为理解原理)
//2进制转换
function baseConverter2(number){
    let stack = new Stack();
    let result = "";
    while(number>0){
        stack.push(number%2);
        number = Math.floor(number/2);
    }
    while(stack.size()){
        result += stack.pop();
    }
    return result||"0";
}
console.log(baseConverter2(8));

//16进制转换
function baseConverter16(number,base=2) {
    let stack = new Stack();
    let sign = "0123456789abcdefg";
    let result = "";
    while (number>0){
        let remainder = number%base;
        stack.push(sign[remainder]);
        number = Math.floor(number/base);
    }
    while(stack.size()){
        result += stack.pop();
    }
    return result||"0";
}
console.log(baseConverter16(15,16));
  • 判断回文字符串
//判断回文
function isPlalindrome(word){
    if( !word || typeof word !== "string" ){
        throw "参数不为字符或字符为空,我们返回了false";
        return false
    };
    let stack = new Stack();
    let back = ""
    stack.push(...word);
    while (stack.size()){
        back += stack.pop();
    }
    return back === word;
}
console.log(isPlalindrome("我爱你你爱我"));
console.log(isPlalindrome("123456"));
  • 检测括号匹配
//检测括号匹配
function ifSignMatch(str){
    let leftSign = "{[(",
        rightSign = "}])";
    let stack = new Stack();
    for(let i=0,len=str.length;i<len;i++){
        let char = str.charAt(i);
        //遇到左括号,入栈
        if( leftSign.indexOf(char) !== -1 ){
            stack.push(char);
        }
        //遇到右括号,匹配
        else if( rightSign.indexOf(char) !== -1 ){
            if( leftSign.indexOf(stack.pop()) !== rightSign.indexOf(char) ){
                return false;
            }
        }
    }
    return stack.size() === 0;
}

console.log(ifSignMatch("{[()]}"));
console.log(ifSignMatch("{[()]}}"));

3. 思考

this.items这可是一个任意都可以直接访问的数组,直接放着也沙雕了吧…想一下有什么样的方式可以避免外界的访问呢?比如 闭包?Symbol ?

三、队列

1. 创建队列结构

队列是一种遵循先进先出(FIFO)原则的有序数据集。

类似场景举例… 入队、出队

下面我们来创建队列数据结构。功能需求:

入队(enqueue)
出队(dequeue)
返回队列首端数据,但不删除(first)
清空队列(clear)
返回队列长度(size)
//队列结构
class Queue{
    constructor(){
        this.items = [];
    }
    enqueue(...rest){
        rest.forEach(v=>{
            this.items.push(v);
        })
    }
    dequeue(){
        return this.items.shift();
    }
    first(){
        return this.items[0];
    }
    clear(){
        this.items = [];
    }
    size(){
        return this.items.length;
    }
}

测试…

优化,避免直接访问items

let Queue = (function(){
    let symbol = Symbol();
    return class{
        constructor(){
            this[symbol] = [];
        }
        enqueue(...rest){
            rest.forEach(v=>{
                this[symbol].push(v);
            });
        }
        dequeue(){
            return this[symbol].shift();
        }
        first(){
            return this[symbol][0];
        }
        clear(){
            this[symbol] = [];
        }
        size(){
            return this[symbol].length;
        }
    }
})();

2. 队列结构的应用

队列是极其基础的一种数据结构,不管在哪都能够运用到,通常都需要配合其他的代码实现强大的功能。比如整个JavaScript的执行机制,因为其实单线程,所以多任务之间都是队列的形式排好然后执行,再借助事件循环实现这个JavaScript强大的功能。比如jQuery中我们学习过的queue队列,最基本的结构就是我们刚刚介绍的队列。所以说我们其实在非常多的地方都在使用着队列,因为它实在太常见太实用了,只不过,使用时的api已经写好的而不需要我们自己来定义实现。

  • 基于队列实现类似于jq的动画队列
//动画队列
let aq = (function(){
    //可以将Queue提出,这样不仅仅在aq这个功能下能用到
    class Queue{
        constructor(){
            this.items = [];
        }
        enqueue(...rest){
            rest.forEach(v=>{
                this.items.push(v);
            });
        }
        dequeue(){
            return this.items.shift();
        }
        first(){
            return this.items[0];
        }
        clear(){
            this.items = [];
        }
        size(){
            return this.items.length;
        }
    }
    //继承Queue,实现一个适合于当前需求的类
    class _Queue extends Queue{
        constructor(){
            super();
            this.ifDone = true;
        }
        run(){
            if( !this.ifDone ){
                return;
            };
            this.ifDone = false;
            !function d(){
                if( this.size() ){
                    new Promise(this.dequeue())
                        .then(d.bind(this));
                }else{
                    this.ifDone = true;
                }
            }.call(this)
        }
        next(){
            this.dequeue()();
        }
    }
    //Map数据结构,用于
    let animateQueue = new Map();
    class Init{
        constructor(selector){
            this.dom = document.querySelector(selector);
        }
        animate(options,time=300){
            if( !animateQueue.get(this.dom)){
                animateQueue.set(this.dom,new _Queue());
            }
            let queue = animateQueue.get(this.dom);
            queue.enqueue(function(res){
                this.style.transition = time/1000+"s";
                this.offsetWidth;
                for (let [key,value] of Object.entries(options)) {
                    this.style[key] = value+'px';
                }
                setTimeout(res,time);
            }.bind(this.dom));
            queue.run();

            return this;
        }
    }

    return function(selector){
        return new Init(selector)
    }
})();

3. 优先队列

根据优先级来决定插入的顺序,就好像登机时的贵宾通道一样…

let PriorityQueue = (function(){
    let symbol = Symbol();
    class Priority{
        constructor(ele,pri){
            this.element = ele;
            this.priority = pri;
        }
    }

    return class{
        constructor(){
            this[symbol] = [];
        }
        enqueue(element,priority){
            let pri = new Priority(element,priority);
            let i=0;
            for (;i<this.size();i++){
                if( this[symbol][i].priority > priority ){
                    break;
                }
            }
            this[symbol].splice(i,0,pri);
        }
        dequeue(){
            return this[symbol].shift().element;
        }
        first(){
            return this[symbol][0].element;
        }
        clear(){
            this[symbol] = [];
        }
        size(){
            return this[symbol].length;
        }
    }
})();

let priorityQueue = new PriorityQueue();
priorityQueue.enqueue("afei",1)
priorityQueue.enqueue("zhuque",0)
priorityQueue.enqueue("wula",1)

console.log(priorityQueue.dequeue());
console.log(priorityQueue.dequeue());
console.log(priorityQueue.dequeue());

此处我们是通过在入队的时候的排序实现优先级的,同样的,我们能不能从出队的角度来实现呢?或者,还有没有什么其他的方式来做呢?

4. 双向队列

允许从前面添加取出,也允许从后面添加取出。

//双向队列
let Queue = (function(){
    let symbol = Symbol();
    return class{
        constructor(){
            this[symbol] = [];
        }
        enqueueFront(ele){
            this[symbol].unshift(ele);
        }
        enqueueBack(ele){
            this[symbol].push(ele);
        }
        dequeueFront(){
            return this[symbol].shift();
        }
        dequeueBack(){
            return this[symbol].pop();
        }
        front(){
            return this[symbol][0];
        }
        back(){
            return this[symbol][this.size()-1];
        }
        size(){
            return this[symbol].length;
        }
    }
})();

四、链表

1. 创建链表结构

存储多个数据常见的结构就是数组,它提供非常方便的[ ]操作符。但是缺点是,当我们往数组中插入或删除数据时,后面所有元素的位置都要进行一次变化。即使提供了便捷的splice函数,但内部原理上还是一样的。所以,当数据较多,并且会频繁的插入/删除数据时,再用数组来充当存储结构效率是较低的。

链表(Linked List)也可以用来存储有序的数据,它与数组的不同在于,并不是在内存中连续的存储,而是每一个节点由自身的数据和指向下一个节点的next组成,通过next指针将所有的节点串联起来形成类似于链的结构。

下面我们来创建链表结构:

链尾添加节点(append)
查找指定数据对应的节点(find)
指定数据后面插入节点(insert)
移除指定数据对应的节点(remove)
打印链表数据(print)
返回链表长度(size)

//可能额外需要的功能
查找数据对应索引(indexOf)
按照索引查找节点(find的参数设计)
按照索引插入节点(insert的参数设计)
按照索引移除节点(remove的参数数据)
//详细分解请看直播课讲解
let LinkedList = (function(){
    let HEAD = Symbol();

    class Node{
        constructor(data){
            this.data = data;
            this.next = null;
        }
    }

    return class{
        constructor(){
            this[HEAD] = null;
        }
        append(data){
            let node = new Node(data);
            let head = this[HEAD];
            if( head === null ){
                this[HEAD]=node;
            }else{
                while(head.next!==null){
                    head = head.next;
                }
                head.next = node;
            }
        }
        find({data,index}){
            let arr = [];
            let head = this[HEAD];
            if(head===null)return arr;
            if( index === undefined ){
                while(head!==null){
                    if( head.data === data ){
                        arr.push(head);
                    }
                    head = head.next;
                }
            }
            if( typeof index === "number" ){
                let i = 0;
                while (head!==null){
                    if( i === index ){
                        arr.push(head);
                    }
                    head = head.next;
                    i ++;
                }
            }
            return arr;
        }
        insert({data,newData,index}){
            let head = this.find({data,index})[0];
            if(!head)return false;

            let node = new Node(newData);
            let prevNext = head.next;
            head.next = node;
            node.next = prevNext;
            return true;
        }
        remove({data,index}){
            let head = this[HEAD];
            if( index === undefined ){
                if( head.data === data ){
                    this[HEAD] = head.next;
                    return;
                }
                while (head.next!==null){
                    if( head.next.data === data ){
                        head.next = head.next.next;
                        return;
                    }
                    head = head.next;
                }
            }
            else if( typeof index === "number" ){
                if( index === 0 ){
                    this[HEAD] = head.next;
                    return;
                }
                let i = 1;
                while(head.next!==null){
                    if( i === index ){
                        head.next = head.next.next;
                        return;
                    }
                    head = head.next;
                    i++;
                }
            }
        }
        print(){
            let head = this[HEAD];
            while (head !== null){
                console.log(head.data);
                head = head.next;
            }
        }
        size(){
            let i = 0;
            let head = this[HEAD];
            while (head !== null){
                i ++;
                head = head.next;
            }
            return i;
        }
    }
})();

2. 双向链表与循环链表

双向链表指每个节点不仅仅有next指针,还有prev执行指向它的前一个节点。

循环链表指最后一个节点的next指针不是指向null,而是指向第一个节点。

代码思路与链表类似,可自己尝试实现。

3. 总结

与数组对比:①.取节点更麻烦,②.获取节点后做对应的添加和移除很方便,……所以当我们需要对数据进行多次的新增和移除操作时,选用链表结构。

五、集合

1. 创建集合结构

集合由一组无序且唯一的数据组成。是的没错,ES6的Set结构就是集合,无需我们自己再实现。接下来我们基于Set数据结构进行讨论。

回顾Set的apiadd delete has clear size,以及初始传参。

2. 并集.交集.差集.子集

  • 并集 — 传入两个集合,返回两个集合的合集
function union(a,b){
    return new Set([...a,...b]);
}
  • 交集 — 传入两个集合,返回两个集合共有的数据集合
function intersection(a,b){
    let set = new Set();
    for (let item of a) {
        if( b.has(item) ){
            set.add(item);
        }
    }
    return set;
}
  • 差集 — 传入两个集合,返回a集合中不存在于b集合的数据集合
function difference(a,b){
    let set = new Set();
    for (let item of a) {
        if( !b.has(item) ){
            set.add(item);
        }
    }
    return set;
}
  • 子集 — 传入两个集合,检测a集合是不是b集合的子集
function subset(a,b){
    for (let item of a) {
        if( !b.has(item) ){
            return false;
        }
    }
    return true;
}

六、字典

1. 创建字典结构

字典是一种以[键,值]的形式存储数据的数据结构,键用来查找查找对应的值。就像字典里面每个字(键)对应的有它的解释(值),再比如电话薄,名字(键)对应电话(值)。

Es6中的Map数据结构就是一种字典。

自己实现字典:

设置数据(set)
获取数据(get)
删除数据(delete)
是否存在(has)
返回长度(size)
let Map = (function(){
    let symbol = Symbol();
    return class{
        constructor(){
            this[symbol] = [];
        }
        set(key,value){
            //遍历以检测是否有存在
            for (let i = 0,len=this.size(); i < len; i++) {
                let item = this[symbol][i];
                if( item[0] === key ){
                    item[1] = value;
                    return;
                }
            }
            this[symbol].push([key,value]);
        }
        get(key){
            for (let i = 0,len=this.size(); i < len; i++) {
                let item = this[symbol][i];
                if( item[0] === key ){
                    return item[1];
                }
            }
            return undefined;
        }
        delete(key){
            for (let i = 0,len=this.size(); i < len; i++) {
                let item = this[symbol][i];
                if( item[0] === key ){
                    this[symbol].splice(i,1);
                    break;
                }
            }
        }
        has(key){
            for (let i = 0,len=this.size(); i < len; i++) {
                let item = this[symbol][i];
                if( item[0] === key ){
                    return true;
                }
            }
            return false;
        }
        size(){
            return this[symbol].length;
        }
    }
})();

let map = new Map();

map.set("afei" , "15575127515");
map.set("zhuque" , "777");
map.set("xinai" , "55555");

map.set("afei" , "155");

console.log(map);
console.log(map.get("zhuque"));
console.log(map.has("xinai"));
console.log(map.has("ZHAOGE"));
map.delete("afei");
console.log(map.size());

七、哈希表

1. 创建哈希表结构

哈希表也叫散列表也是一种基于键值对存储值的一种数据结构,与之前的区别在于,哈希表能快速的定位值的位置,而不需要逐个遍历匹配。

JavaScript中的对象就具有哈希表结构的特性

先忘记掉js的对象,我们通过数组来模拟一个哈希表。通过一系列的计算通过key值得到对应的序号,这个计算过程是建立哈希表最重要的过程,叫做哈希函数。比如我们可以通过每个字符的ANSII码相加得到对应的哈希索引:

通过ANSII码来得到哈希值
let HashTable = (function(){
    let symbol = Symbol();
    let hashCode = function(key){
        let hash = 0;
        for(let i=0;i<key.length;i++){
            hash += key.charCodeAt(i);
        }
        return hash;
    };
    return class{
        constructor(){
            this[symbol] = [];
        }
        set(key,value){
            this[symbol][hashCode(key)] = value;
        }
        get(key){
            return this[symbol][hashCode(key)];
        }
    }
})();

let h1 = new HashTable;
h1.set("afei","15575127515")
h1.set("zhuque","15714565897")
h1.set("xinai","15666666666")

console.log(h1);

很显然,这个哈希函数是很不合理的,第一,哈希值太大,只需要存三个数据却建立了一个长度好几百的数组,一般合理的利用率范围是 0.6-0.9,也就是说数据个数与总长度的比例在这之间。第二,容易出现重复,要是某两个字符码加起来刚好一样就会出现哈希值重复,从而覆盖前一个数据。

//解决第一个问题,我们可以预先设置一个较为合理的数组长度,然后规定序号不会超过长度:
//解决第二个问题,可以结合我们讲过的链表结构来存储,或者线性探查逐位往后推的方法:

//设定长度37,使用链表来解决冲突(使用质数来定长度是最合适的选择)
let LinkedList = (function(){
    let HEAD = Symbol();

    class Node{
        constructor(data){
            this.data = data;
            this.next = null;
        }
    }

    return class{
        constructor(){
            this[HEAD] = null;
        }
        append(data){
            let node = new Node(data);
            let head = this[HEAD];
            if( head === null ){
                this[HEAD]=node;
            }else{
                while(head.next!==null){
                    head = head.next;
                }
                head.next = node;
            }
        }
        find({data,index}){
            let arr = [];
            let head = this[HEAD];
            if(head===null)return arr;
            if( index === undefined ){
                while(head!==null){
                    if( head.data === data ){
                        arr.push(head);
                    }
                    head = head.next;
                }
            }
            if( typeof index === "number" ){
                let i = 0;
                while (head!==null){
                    if( i === index ){
                        arr.push(head);
                    }
                    head = head.next;
                    i ++;
                }
            }
            return arr;
        }
        insert({data,newData,index}){
            let head = this.find({data,index})[0];
            if(!head)return false;

            let node = new Node(newData);
            let prevNext = head.next;
            head.next = node;
            node.next = prevNext;
            return true;
        }
        remove({data,index}){
            let head = this[HEAD];
            if( index === undefined ){
                if( head.data === data ){
                    this[HEAD] = head.next;
                    return;
                }
                while (head.next!==null){
                    if( head.next.data === data ){
                        head.next = head.next.next;
                        return;
                    }
                    head = head.next;
                }
            }
            else if( typeof index === "number" ){
                if( index === 0 ){
                    this[HEAD] = head.next;
                    return;
                }
                let i = 1;
                while(head.next!==null){
                    if( i === index ){
                        head.next = head.next.next;
                        return;
                    }
                    head = head.next;
                    i++;
                }
            }
        }
        print(){
            let head = this[HEAD];
            while (head !== null){
                console.log(head.data);
                head = head.next;
            }
        }
        size(){
            let i = 0;
            let head = this[HEAD];
            while (head !== null){
                i ++;
                head = head.next;
            }
            return i;
        }
        getHead(){
            return this[HEAD];
        }
    }
})();

let HashTable = (function(){
    let symbol = Symbol();
    let hashCode = function(key){
        let hash = 0;
        for(let i=0;i<key.length;i++){
            hash += key.charCodeAt(i);
        }
        return hash % 37;
    };
    let ValuePair = class{
        constructor(key,value){
            this.key = key;
            this.value = value;
        }
    }
    return class{
        constructor(){
            this[symbol] = [];
        }
        set(key,value){
            let index = hashCode(key);
            if( !this[symbol][index] ){
                this[symbol][index] = new LinkedList();
            }
            this[symbol][index].append(new ValuePair(key,value));
        }
        get(key){
            let index = hashCode(key);
            let linked = this[symbol][index];
            if( !linked )return undefined;
            let head = linked.getHead();
            while (head){
                if( head.data.key === key ){
                    return head.data.value;
                }
                head = head.next;
            }
            return undefined;
        }
    }
})();

let h1 = new HashTable;
h1.set("afei","15575127515")
h1.set("zhuque","15714565897")
h1.set("xinai","15666666666")
h1.set("feia","15666666666")

console.log(h1);

console.log(h1.get("afei"));
console.log(h1.get("feia"));
//向下线性探索,解决冲突问题

let HashTable = (function(){
    let symbol = Symbol();
    let hashCode = function(key){
        let hash = 0;
        for(let i=0;i<key.length;i++){
            hash += key.charCodeAt(i);
        }
        return hash % 37;
    };
    let ValuePair = class{
        constructor(key,value){
            this.key = key;
            this.value = value;
        }
    }
    return class{
        constructor(){
            this[symbol] = [];
        }
        set(key,value){
            let index = hashCode(key);
            while(this[symbol][index]){
                if( this[symbol][index].key === key )break;
                index ++;
                index %= 37;
            }
            this[symbol][index] = new ValuePair(key,value);
        }
        get(key){
            let index = hashCode(key);
            let baseIndex = index;
            if(!this[symbol][index])return undefined;
            while (this[symbol][index]){
                if( this[symbol][index].key === key ){
                    return this[symbol][index].value;
                }
                index ++;
                index %= 37;
                if( baseIndex === index )return undefined;
            }
        }
    }
})();

let h1 = new HashTable;
h1.set("afei","15575127515")
h1.set("zhuque","15714565897")
h1.set("xinai","15666666666")
h1.set("feia","15666666666")

console.log(h1);

console.log(h1.get("afei"));
console.log(h1.get("feia"));

2. 有名的哈希函数

刚刚我们写的hash函数并不优秀,因为出现重复的概率比较高。下面列出一些程序猿前辈们总结的好用的hash函数,大家可以先感受一下

//DJB
function DJBHash(str)    {    
    var hash = 5381;   
    var len = str.length , i = 0

    while (len--){    
        hash = (hash << 5) + hash + str.charCodeAt(i++); /* times 33 */    
    }    
    hash &= ~(1 << 31); /* strip the highest bit */    
    return hash;    
}
//JS
function JSHash(str)  {  
      var hash = 1315423911;  
      for(var i = 0; i < str.length; i++)  {  
         hash ^= ((hash << 5) + str.charCodeAt(i) + (hash >> 2));  
      }  
      return hash;  
}  
//PJW
function PJWHash( str)  {  
      var BitsInUnsignedInt = 4 * 8;  
      var ThreeQuarters     =  (BitsInUnsignedInt  * 3) / 4;  
      var OneEighth         = (BitsInUnsignedInt / 8);  
      var HighBits          = (0xFFFFFFFF) << (BitsInUnsignedInt - OneEighth);  
      var hash              = 0;  
      var test              = 0;  
      for(var i = 0; i < str.length; i++)  {  
         hash = (hash << OneEighth) + str.charCodeAt(i);  
         if((test = hash & HighBits)  != 0)  
         {  
            hash = (( hash ^ (test >> ThreeQuarters)) & (~HighBits));  
         }  
      }  
      return hash;  
} 

……

八、树

1. 了解树结构

树是一种重要的非线性结构,以分层的方式存储数据,比如公司的组织架构。

 

顶部节点叫做根节点,其他节点都是它的子节点,有子元素的节点称为内部节点,没有子元素的节点称为外部节点或叶节点。每个节点有层次,比如图中B节点层次为1,A节点层次为0,树种最大的层次称为该树的深度。每个节点和它的子节点组成子树

2. 二叉树与二叉搜索树

二叉树在计算机科学中应用非常广泛。它是一种特殊的树,每个节点最多拥有左右两个子节点。利用它的特性我们可以更高效的对树中数据进行 增 删 查等操作。

二叉搜索树(BinarySearchTree)是二叉树的一种,它只允许在节点的左侧存储比节点小的值,在节点右侧存储比节点大(或等于)的值。

实现二叉搜索树:

插入节点 insert
let BST = (function(){
    class Node{
        constructor(data){
            this.data = data;
            this.left = null;
            this.right = null;
        }
    }
    let root = Symbol();
    function insertNode(node,root) {
        if (node.data < root.data) {
            if (root.left) {
                insertNode(node,root.left);
            } else {
                root.left = node;
            }
        }else{
            if( root.right ){
                insertNode(node,root.right);
            }else{
                root.right = node;
            }
        }
    }
    return class{
        constructor(){
            this[root] = null;
        }
        insert(data){
            let node = new Node(data);
            if (this[root] !== null) {
                insertNode(node,this[root]);
            } else {
                this[root] = node;
            }
        }
    }
})();

let bst = new BST();

bst.insert(9);
bst.insert(5);
bst.insert(4);
bst.insert(51);
bst.insert(88);
bst.insert(76);
bst.insert(23);
bst.insert(7);
bst.insert(1);

console.log(bst);

3. 树的遍历

讲课的时候分开讲每一种遍历的代码,课件里面代码我全放后面……

  • 中序遍历针对每棵树都是左根右的顺序遍历数据:

 

  • 先续遍历针对每棵树都是根左右的顺序遍历:团懒得画了
  • 后续遍历针对每棵树都是左右根的顺序遍历:懒得画+1
let BST = (function(){
    class Node{
        constructor(data){
            this.data = data;
            this.left = null;
            this.right = null;
        }
    }
    let root = Symbol();
    function insertNode(node,root) {
        if (node.data < root.data) {
            if (root.left) {
                insertNode(node,root.left);
            } else {
                root.left = node;
            }
        }else{
            if( root.right ){
                insertNode(node,root.right);
            }else{
                root.right = node;
            }
        }
    }
    function inOrderTraverseNode(node,arr){
        if( node === null )return;
        inOrderTraverseNode(node.left,arr);
        arr.push(node.data);
        inOrderTraverseNode(node.right,arr);
    }
    function preOrderTraverseNode(node,arr){
        if( node === null )return;
        arr.push(node.data);
        preOrderTraverseNode(node.left,arr);
        preOrderTraverseNode(node.right,arr);
    }
    function postOrderTraverseNode(node,arr){
        if( node === null )return;
        postOrderTraverseNode(node.left,arr);
        postOrderTraverseNode(node.right,arr);
        arr.push(node.data);
    }
    return class{
        constructor(){
            this[root] = null;
        }
        insert(data){
            let node = new Node(data);
            if (this[root] !== null) {
                insertNode(node,this[root]);
            } else {
                this[root] = node;
            }
        }
        inOrderTraverse(){
            let arr = [];
            inOrderTraverseNode(this[root],arr);
            return arr;
        }
        preOrderTraverse(){
            let arr = [];
            preOrderTraverseNode(this[root],arr);
            return arr;
        }
        postOrderTraverse(){
            let arr = [];
            postOrderTraverseNode(this[root],arr);
            return arr;
        }
    }
})();

let bst = new BST();

bst.insert(9);
bst.insert(5);
bst.insert(4);
bst.insert(51);
bst.insert(88);
bst.insert(76);
bst.insert(23);
bst.insert(7);
bst.insert(1);

console.log(bst);

console.log(bst.inOrderTraverse()); //在二叉搜索树种,这种方式正好可以得到顺序排列的数组
console.log(bst.preOrderTraverse());
console.log(bst.postOrderTraverse());

4. 查找与删除

//获取最小值  getMin
//获取最大值  getMax
//检测值是否存在  has
//移除值  remove
let BST = (function(){
    let ROOT = Symbol();
    class Node{
        constructor(data){
            this.data = data;
            this.left = null;
            this.right = null;
        }
    }
    function insertNode(node,root) {
        if (node.data < root.data) {
            if (root.left) {
                insertNode(node,root.left);
            } else {
                root.left = node;
            }
        }else{
            if( root.right ){
                insertNode(node,root.right);
            }else{
                root.right = node;
            }
        }
    }
    function inOrderTraverseNode(node,arr){
        if( node === null )return;
        inOrderTraverseNode(node.left,arr);
        arr.push(node.data);
        inOrderTraverseNode(node.right,arr);
    }
    function preOrderTraverseNode(node,arr){
        if( node === null )return;
        arr.push(node.data);
        preOrderTraverseNode(node.left,arr);
        preOrderTraverseNode(node.right,arr);
    }
    function postOrderTraverseNode(node,arr){
        if( node === null )return;
        postOrderTraverseNode(node.left,arr);
        postOrderTraverseNode(node.right,arr);
        arr.push(node.data);
    }
    function hasData(node,data) {
        if( !node )return false;
        if( data<node.data ){
            return hasData(node.left,data);
        }else if( data>node.data ){
            return hasData(node.right,data);
        }else{
            return false;
        }
    }
    function getMin(node){
        while(node.left){
            node = node.left;
        }
        return node;
    }
    function removeNode(node,data) {
        if( !node )return node;
        if( node.data < data ){
            node.right = removeNode(node.right,data);
        }else if( node.data > data ){
            node.left = removeNode(node.left,data);
        }else{
            if( node.left === null && node.right === null ){
                node = null;
                return node;
            }
            if( node.left === null){
                node = node.right;
                return node;
            }
            if( node.right === null ){
                node = node.left;
                return node;
            }
            if( node.left && node.right ){
                //方案1:左树最大值 或者 右树最小值 充当删除的这个值的位置
                //方案2:把左树放置到右树的最左端
                //方案3:把右树放置到左树的最右端

                //方案1 —— 
                let min = getMin(node.right);
                node.data = min.data;
                node.right = removeNode(node.right,min.data);
                return node;
            }
        }
    }
    return class{
        constructor(){
            this[ROOT] = null;
        }
        insert(data){
            let node = new Node(data);
            if (this[ROOT] !== null) {
                insertNode(node,this[ROOT]);
            } else {
                this[ROOT] = node;
            }
        }
        inOrderTraverse(){
            let arr = [];
            inOrderTraverseNode(this[ROOT],arr);
            return arr;
        }
        preOrderTraverse(){
            let arr = [];
            preOrderTraverseNode(this[ROOT],arr);
            return arr;
        }
        postOrderTraverse(){
            let arr = [];
            postOrderTraverseNode(this[ROOT],arr);
            return arr;
        }
        getMin(){
            let root = this[ROOT];
            if(!root)return null;
            while (root.left)root = root.left;
            return root.data;
        }
        getMax(){
            let root = this[ROOT];
            if(!root)return null;
            while (root.right)root = root.right;
            return root.data;
        }
        has(data){
            return hasData(this[ROOT],data);
        }
        remove(data){
            this[ROOT] = removeNode(this[ROOT],data);
        }
    }
})();

5. 平衡二叉树

二叉搜索树有可能出现的一种情况是某一条分支深度特别大,而其他的分支深度很低。为了解决这个问题,有一种树叫做平衡二叉树(作者是 Adelson Velskii 和 Landi,所以也叫AVL树)。自平衡树在添加节点时,会按情况分配,保证树中任意节点的左子树和右子树的深度差不超过1。

创建平衡二叉树:

let AVL = (function(){
    let ROOT = Symbol();
    class Node{
        constructor(key){
            this.key = key;
            this.left = null;
            this.right = null;
        }
    }

    //插入节点
    function insertNode(root,node) {
        if( !root )return node;
        if( node.key < root.key ){
            root.left = insertNode(root.left,node);

            //检测树深度失衡
            if( NodeDeep(root.left) - NodeDeep(root.right) > 1 ){
                if( node.key < root.left.key ){
                    //左左
                    root = LL(root);
                }else{
                    //左右
                    root = LR(root);
                }

            }
        }else{
            root.right = insertNode(root.right,node);
            //检测树深度失衡
            if( NodeDeep(root.right) - NodeDeep(root.left) > 1 ){
                if( node.key < root.right.key ){
                    //右左
                    root = RL(root);
                }else{
                    //右右
                    root = RR(root);
                }
            }
        }
        return root;
    }

    //计算树深度
    function NodeDeep(node) {
        if(!node)return 0;
        return Math.max(NodeDeep(node.right),NodeDeep(node.left)) + 1;
    }

    //第一种情况,左左 LL
    function LL(node) {
        let tmp = node.left;
        node.left = tmp.right;
        tmp.right = node;
        return tmp;
    }

    //第二种情况,右右 RR
    function RR(node) {
        let tmp = node.right;
        node.right = tmp.left;
        tmp.left = node;
        return tmp;
    }

    //第三种情况,左右 LR
    function LR(node) {
        node.left = RR(node.left);
        return LL(node);
    }

    //第四种情况,右左 RL
    function RL(node) {
        node.right = LL(node.right);
        return RR(node);
    }

    return class {
        constructor(){}
        insert(key){
            this[ROOT] = insertNode(this[ROOT],new Node(key));
        }
    }
})();

let avl = new AVL;

avl.insert(50)
avl.insert(30)
avl.insert(60)
avl.insert(58)
avl.insert(80)
avl.insert(59)
avl.insert(90)
avl.insert(100)

console.log(avl);

6. 红黑树

红黑树(Red Black Tree) 是平衡二叉树的一种。红黑树是一种特化的AVL树,在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的。 它可以在O(log n)时间内做查找,插入和删除,这里的 n 是树中元素的数目。

红黑树的特点:

  1. 节点必须是红色或黑色
  2. 根节点是黑色
  3. 所有叶子是都黑色(叶子是指null节点)
  4. 每个红色节点的子节点都是黑色(不能有连续的红节点)
  5. 任一节点到其每个叶子的所有路径都包含相同数量的黑色节点

红黑树例图:

img

创建红黑树:


// 红黑树模型
class RedBlackTree {
  constructor() {
    this.root = null;
  }
  // 插入节点
  insert(key) {
    let newNode = new Node(key);
    let current = this.root;
    let parent = null;
    let color = RED;
    // 如果根节点为null,则新节点为根节点
    if (this.root === null) {
      this.root = newNode;
      newNode.color = BLACK;
      return;
    }
    // 找到新节点的父节点
    while (current !== null) {
      parent = current;
      if (newNode.key 

九、图

1. 了解图结构

节点(顶点)组成,节点之间由边连接,一个边连接两个节点,一个节点可以对应很多条边(这些边的数量称为这个节点的)。相邻节点按顺序组成的序列叫路径。路径中没有重复节点的叫做简单路径。路径中最后一个顶点和第一个顶点相同的构成有向图的边是带箭头的(有序的)。每个节点之间都有边的图叫强连通图

2. 创建图

要存储图中的节点,并且表明节点间边的关系,我们可以采用多种方式来实现。常见的有邻接矩阵邻接表。邻接矩阵使用二维数组来描述节点间连接关系,但是当节点比边多很多时,会有很多空位浪费。邻接表采用字典(也可以是表,数组)来存储节点和它对应连接的节点关系,具体模型图请看课程讲解。

使用邻接表的方案来实现图:

 //图结构 类
let Graph = class  {
    constructor(){
        //存储所有顶点
        this.vertices = [];
        //存储每个顶点对应的相邻顶点(边)
        this.edges = {};
    }
    //添加顶点
    addVertex(...rest){
        rest.forEach(v=>{
            //如果顶点已经添加,return
            if( this.vertices.includes(v) )return;

            //顶点不存在
            //添加顶点
            this.vertices.push(v);
            //初始化顶点的边存储信息
            this.edges[v] = [];
        });
    }
    //添加边
    addEdge(v1,v2){
        //添加节点
        this.addVertex(v1,v2);
        //如果双方已经添加过对应的信息,返回
        if( this.edges[v1].includes(v2) )return;
        //双方互相添加边信息
        this.edges[v1].push(v2);
        this.edges[v2].push(v1);
    }
};

let graph = new Graph;

graph.addEdge("A" , "B");
graph.addEdge("A" , "C");
graph.addEdge("A" , "E");
graph.addEdge("B" , "D");
graph.addEdge("B" , "E");
graph.addEdge("C" , "F");
graph.addEdge("E" , "F");

console.log(graph);

3. 遍历

图的遍历有两种算法:广度优先(Breadth-First Search)\深度优先(Depth-First Search)。遍历图可以查找某个顶点、寻找两个顶点间的路径、检测图是否连通、检测图是否有环等。

遍历思路:将各个顶点分为三种状态-白、灰、黑,白色代表未被发现,灰色代表已被发现但未被探索,黑色代表已被发现并被探索。广度优先遍历过程是,先发现节点所有相邻节点,再遍历这些节点继续按规则往下遍历。深度优先遍历过程是,找到一个相邻节点后,立即再继续找它的相邻节点,以此类推。

//图结构 类
let Graph = (function(){
    //队列类,辅助广度优先遍历
    class Queue{
        constructor(){
            this.items = [];
        }
        enqueue(data){
            this.items.push(data);
        }
        dequeue(){
            return this.items.shift();
        }
        size(){
            return this.items.length;
        }
    }

    //图类
    return class {
        constructor(){
            //存储所有顶点
            this.vertices = [];
            //存储每个顶点对应的相邻顶点(边)
            this.edges = {};
        }
        //添加顶点
        addVertex(...rest){
            rest.forEach(v=>{
                //如果顶点已经添加,return
                if( this.vertices.includes(v) )return;

                //顶点不存在
                //添加顶点
                this.vertices.push(v);
                //初始化顶点的边存储信息
                this.edges[v] = [];
            });
        }
        //添加边
        addEdge(v1,v2){
            //添加节点
            this.addVertex(v1,v2);
            //如果双方已经添加过对应的信息,返回
            if( this.edges[v1].includes(v2) )return;
            //双方互相添加边信息
            this.edges[v1].push(v2);
            this.edges[v2].push(v1);
        }
        //广度优先
        bfs(v){
            let vertices = this.vertices,
                edges = this.edges,
                color = {},
                queue = new Queue()
            ;

            //对每个顶点添加状态标志
            vertices.forEach(v=>{
                color[v] = "white";
            });

            //入口顶点入队及状态改变
            queue.enqueue(v);
            color[v] = "grey";

            //循环出队进行发现或探索
            while (queue.size()){
                //取出队首顶点
                let u = queue.dequeue();
                //获取顶点对应的相邻顶点
                let edg = edges[u];

                //测试打印
                let s = u+"===>"
                //探索
                edg.forEach(v=>{
                    if(color[v] !== "white")return;
                    s += v+" "
                    queue.enqueue(v);
                    color[v] = "grey";
                });
                //探索完,改变状态
                color[u] = "black";

                console.log(s);
            }
        }
    };
})();

let graph = new Graph;

graph.addEdge("A" , "B");
graph.addEdge("A" , "C");
graph.addEdge("A" , "E");
graph.addEdge("B" , "D");
graph.addEdge("B" , "E");
graph.addEdge("C" , "F");
graph.addEdge("E" , "F");

graph.bfs("A");
//图结构 类
let Graph = (function(){
    //队列类,辅助广度优先遍历
    class Queue{
        constructor(){
            this.items = [];
        }
        enqueue(data){
            this.items.push(data);
        }
        dequeue(){
            return this.items.shift();
        }
        size(){
            return this.items.length;
        }
    }

    //图类
    return class {
        constructor(){
            //存储所有顶点
            this.vertices = [];
            //存储每个顶点对应的相邻顶点(边)
            this.edges = {};
        }
        //添加顶点
        addVertex(...rest){
            rest.forEach(v=>{
                //如果顶点已经添加,return
                if( this.vertices.includes(v) )return;

                //顶点不存在
                //添加顶点
                this.vertices.push(v);
                //初始化顶点的边存储信息
                this.edges[v] = [];
            });
        }
        //添加边
        addEdge(v1,v2){
            //添加节点
            this.addVertex(v1,v2);
            //如果双方已经添加过对应的信息,返回
            if( this.edges[v1].includes(v2) )return;
            //双方互相添加边信息
            this.edges[v1].push(v2);
            this.edges[v2].push(v1);
        }
        //最短路径
        BFS(v){
            let vertices = this.vertices,
                edges = this.edges,
                color = [],
                queue = new Queue(),
                info = {}
            ;

            //状态初始化
            vertices.forEach(v=>{
                color[v] = "white";
            });

            //入口顶点入队 状态改变 初始信息
            queue.enqueue(v);
            color[v] = "grey";
            info[v] = {distance : 0, path : v};

            //发现与探索
            while (queue.size()){
                let u = queue.dequeue();
                edges[u].forEach(v=>{
                    if(color[v]!=="white")return;
                    color[v] = "grey";
                    queue.enqueue(v);
                    info[v] = {
                        distance : info[u].distance + 1,
                        path : info[u].path + "->" + v
                    };
                });
                color[u] = "black";
            }

            /*
                        返回信息的期望状态
                        {
                            "A" : {distance:0,path:"A"}
                            "B" : {distance:1,path:"A-B"}
                            "C" : {distance:1,path:"A-C"}
                            "D" : {distance:2,path:"A-B-D"}
                            "E" : {distance:1,path:"A-E"}
                            "F" : {distance:2,path:"A-C-F"}
                        }
                         */
            return info;
        }
    };
})();

let graph = new Graph;

graph.addEdge("A" , "B");
graph.addEdge("A" , "C");
graph.addEdge("A" , "E");
graph.addEdge("B" , "D");
graph.addEdge("B" , "E");
graph.addEdge("C" , "F");
graph.addEdge("E" , "F");

console.log(graph.BFS("D"));


评论(0)

查看评论列表

暂无评论


发表评论

表情 颜文字
插入代码