JS数据结构与算法-读书小记

110 阅读28分钟

参考: 《JS数据结构与算法》

ECMAScript 6 和数组的新功能

方法描述
@@iterator返回一个包含数组键值对的迭代器对象,可以通过同步调用得到数组元素的键值对
entries返回包含数组所有键值对的@@iterator
values返回包含数组中所有值的@@iterator
keys返回包含数组所有索引的@@iterator
copyWithin(要复制到数组的某个位置,复制数组开始位置,结束位置)复制数组中一系列元素到同一数组指定的起始位置,不包括结束位置
includes(所查找的元素,指定位置)如果数组中存在某个元素则返回true,否则返回false。ES7新增
find根据回调函数给定的条件从数组中查找元素,如果找到则返回该元素
findIndex根据回调函数给定的条件从数组中查找元素,如果找到则返回该元素在数组中的索引
Array.fill(填充元素,开始位置,结束位置)用静态值填充数组,不包括结束位置
Array.from(已有数组,回调函数)根据已有数组创建一个新数组,第二个参数为回调函数
Array.of(参数1,...)根据传入的参数创建一个新数组,

栈数据结构

  • 利用数组模拟栈,代码如下:
function Stack(){
    let items = [];
  	//向栈中添加元素
    this.push = function(element){
        items.push(element)
    };
  	//从栈中移除元素
    this.pop = function(){
        return items.pop();
    };
  	//查看栈顶元素
    this.peek = function(){
        return items[items.length-1];
    };
  	//检查栈是否为空
    this.isEmpty = function(){
        return items.length == 0;
    };
  	//清空
    this.clear = function(){
        items = [];
    };
  	//打印栈元素
    this.print = function(){
        console.log(items.toString());
    };
}
module.exports = Stack;
  • 用栈解决问题
    • 从十进制到二进制,代码如下:
const Stack = require('./stack')

// 十进制转换二进制
function divideBy2(decNumber){
    let remStack = new Stack(),
      rem,
      binaryString = '';
    while (decNumber > 0){ 
        rem = Math.floor(decNumber % 2); 
        remStack.push(rem); 
        decNumber = Math.floor(decNumber / 2); 
    }
        while (!remStack.isEmpty()){ 
        binaryString += remStack.pop().toString();
    }
    return binaryString;
}
console.log(divideBy2(10))
    • 十进制转换其它进制(二、八、十六进制)
const Stack = require('./stack')

function baseConverter(decNumber, base){
    let remStack = new Stack(),
      rem,
      baseString = '',
      digits = '0123456789ABCDEF'; 
    while (decNumber > 0){
        rem = Math.floor(decNumber % base);
        remStack.push(rem);
        decNumber = Math.floor(decNumber / base);
    }
        while (!remStack.isEmpty()){
        baseString += digits[remStack.pop()]; 
    }
    return baseString;
}
console.log(baseConverter(100345, 16));

队列

  • 利用数组模拟栈,代码如下:
function Queue(){
    let items = [];
		//向队列中添加元素
    this.enqueue = function(ele){
        items.push(ele)
    };
		//从队列移除元素
    this.dequeue = function(){
        return items.shift()
    };
		//查看队头元素
    this.front = function(){
        return items[0]
    };
		//检查队列是否为空
    this.isEmpty = function(){
        return items.length == 0
    };
		//返回队列大小
    this.size = function(){
        return items.length;
    };
		//打印队列元素
    this.print = function(){
        console.log(items.toString());
    };
}
module.exports = Queue;
  • 优先队列:元素的添加和移除是基于优先级的。实现一个优先队列,有两种选项:设置优先级,然后在正确的位置添加元素;或者用入列操作添加元素,然后按照优先级移除它们。
// 优先队列
function PriorityQueue() {
    let items = [];
    function QueueElement (element, priority){ 
      this.element = element;
      this.priority = priority;
    }
    this.enqueue = function(element, priority){
      let queueElement = new QueueElement(element, priority);
      let added = false;
      for (let i=0; i<items.length; i++){
        if (queueElement.priority < items[i].priority){ 
            items.splice(i,0,queueElement); 
            added = true;
            break; 
        } }
        if (!added){
            items.push(queueElement); 
        } 
    };
    this.dequeue = function(){
        return items.shift()
    };

    this.front = function(){
        return items[0]
    };

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

    this.print = function(){
    for (let i=0; i<items.length; i++){
        console.log(`${items[i].element} -
        ${items[i].priority}`);
    }
  };
  //其他方法和默认的Queue实现相同 
}
let priorityQueue = new PriorityQueue();
priorityQueue.enqueue("John", 1);
priorityQueue.enqueue("Jack", 3);
priorityQueue.enqueue("Camila", 2);
priorityQueue.enqueue("Camila", 0);
priorityQueue.print();
  • 循环队列-击鼓传花
//循环队列 - 击鼓传花
let Queue = require('./queue')
function hotPotato(nameList,num){
    let queue = new Queue();
    
    for(let i=0; i<nameList.length; i++){
        queue.enqueue(nameList[i])
    }

    let eliminated = '';
    while(queue.size()>1){
        for(let i=0; i<num; i++){
            queue.enqueue(queue.dequeue())
        }
        eliminated = queue.dequeue();
        console.log(eliminated + '在击鼓传花游戏中被淘汰。');
    }
    return queue.dequeue();
}

let names = ['John','Jack','Camila','Ingrid','Carl'];
let winner = hotPotato(names, 7);
console.log('The winner is: ' + winner);

//第一轮 'Camila' 淘汰; size = 4
//  1.['Jack','Camila','Ingrid','Carl','John']
//  2.['Camila','Ingrid','Carl','John','Jack']
//  3.['Ingrid','Carl','John','Jack','Camila']
//  4.['Carl','John','Jack','Camila','Ingrid']
//  5.['John','Jack','Camila','Ingrid','Carl']
//  6.['Jack','Camila','Ingrid','Carl','John']
//  7.['Camila','Ingrid','Carl','John','Jack']

 //第二轮 'Jack' 淘汰 size = 3
// 1.['Carl','John','Jack','Ingrid']
// 2.['John','Jack','Ingrid','Carl']
// 3.['Jack','Ingrid','Carl','John']
// 4.['Ingrid','Carl','John','Jack']
// 5.['Carl','John','Jack','Ingrid']
// 6.['John','Jack','Ingrid','Carl']
// 7.['Jack','Ingrid','Carl','John']
// ......

  • JavaScript 任务队列:待总结

链表

链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个 元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。

相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。链表需要使用指针。数组的另一个细节是可以直接访问任何 位置的任何元素,而要想访问链表中间的一个元素,需要从起点(表头)开始迭代列表直到找到 所需的元素。

  • 创建链表
function LinkedList(){
    /**
     *  Node类表示要加入列表的某个节点
     *  element:要添加到列表的值
     *  next:指向列表中下一个节点项的指针 
     */ 
    let Node = function(element){
        this.element = element; 
        this.next = null;
    };

    let length = 0; // 存储列表项的数量
    let head = null; // 存储第一个节点的引用

    // 向列表尾部添加一个新的项
    this.append = function(element){
        let node = new Node(element),current;
        if(head === null){ // 列表中的第一个节点
            head = node;
        }else{
            current = head;
            // 循环列表,直到找到最后一项
            while(current.next){
                current = current.next;
            }
            // 找到最后一项,将其next赋为node,建立链接
            current.next = node;
        }
        length++; // 更新列表的长度
    };

    // 向列表的任意位置插入一个新的项
    this.insert = function(pos,element){
        // 检查越界值
        if(pos >=0 && pos <= length){
            let node = new Node(element),
                current = head,
                previous,
                index = 0;
            if(pos === 0){ //在第一个位置添加
                node.next = current; //{2}
                head = node;
            }else{
                while(index++ < pos){ //{3}
                    previous = current;
                    current = current.next;
                }
                node.next = current; //{4}
                previous.next = node; //{5}
            }
            length++; //更新列表的长度
            return true;
        }else{
            return false
        }
    };

    this.removeAt = function(pos){
        /**
         * 如果想移除第一个元素,要做的就是让head指向列表的第二个元素 
         * 要从列表中移除当前元素/最后一个元素,要做的就是将previous.next和current.next链接起来
         * */
        // 检查越界值
        if(pos > -1 && pos < length){ // {1}
            let current = head, // {2} 对列表中第一个元素的引用
                previous, // {3} 当前元素的前一个元 素的引用
                index = 0; // {4}
            //移除第一项
            if(pos === 0){ // {5}
                head = current.next;
            }else{
                while(index++ < pos){ // {6}
                    previous = current; // {7}
                    current = current.next; // {8}
                }
                // 将previous与current 的下一项链接起来:跳过current,从而移除它
                previous.next = current.next; // {9}
            }
            length--; // {10}
            return current.element;
        }else{
            return null;
        }
    };

    // 从列表中移除一项
    this.remove = function(element){
        let index = this.indexOf(element);
        return this.removeAt(index);
    };

    // 返回元素在列表中的索引。如果列表中没有该元素则返回-1
    this.indexOf = function(element){
        let current = head,
            index = -1;
        while(current){
            if(element === current.element){
                return index;
            }
            index++;
            current = current.next
        }
        return -1;
    };

    // 如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false
    this.isEmpty = function(){
        return length === 0;
    };

    // 返回链表包含的元素个数
    this.size = function(){
        return length;
    };

    this.getHead = function(){
        return head;
    };

    // 由于列表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值
    this.toString = function(){
        let current = head,
            string = '',
            index = 0;
        while(current){
            string += current.element + (current.next ? `--第${index}个元素\n` : '');
            current = current.next;
            index++;
        }
        return `${string}--第${index}个元素`;
    };

}
  • 双向链表
function DoublyLinkedList(){
    /**
     *  Node类表示要加入列表的某个节点
     *  element:要添加到列表的值
     *  next:指向列表中下一个节点项的指针 
     */ 
    let Node = function(element){
        this.element = element; 
        this.next = null;
        this.prev = null; //新增的
    };

    let length = 0; // 存储列表项的数量
    let head = null; // 存储第一个节点的引用
    let tail = null; // 新增的 对列表最后一项的引用

    // 向列表尾部添加一个新的项
    this.append = function(element){
        let node = new Node(element),current;
        if(head === null){ // 列表中的第一个节点
            head = node;
        }else{
            current = head;
            // 循环列表,直到找到最后一项
            while(current.next){
                current = current.next;
            }
            // 找到最后一项,将其next赋为node,建立链接
            current.next = node;
        }
        length++; // 更新列表的长度
    };

    // 向列表的任意位置插入一个新的项
    this.insert = function(pos,element){
        // 检查越界值
        if(pos >=0 && pos <= length){
            let node = new Node(element),
                current = head,
                previous,
                index = 0;
            if(pos === 0){ //在第一个位置添加
                if(!head){ //新增的 {1}
                    head = node;
                    tail = node;
                }else{
                    node.next = current; 
                    current.prev = node; //新增的 {2}
                    head = node;
                }
            }else if(pos === length){ //最后一项 //新增的
                current = tail; //{3}
                current.next = node;
                node.prev = current;
                tail = node;
            }else{
                while(index++ < pos){ //{4}
                    previous = current;
                    current = current.next;
                }
                node.next = current; //{5}
                previous.next = node; 

                current.prev = node; //新增的
                node.prev = previous; //新增的
            }
            length++; //更新列表的长度
            return true;
        }else{
            return false
        }
    };

    this.removeAt = function(pos){
        /**
         * 如果想移除第一个元素,要做的就是让head指向列表的第二个元素 
         * 要从列表中移除当前元素/最后一个元素,要做的就是将previous.next和current.next链接起来
         * */
        // 检查越界值
        if(pos > -1 && pos < length){ 
            let current = head, // 对列表中第一个元素的引用
                previous, // 当前元素的前一个元 素的引用
                index = 0; 
            //移除第一项
            if(pos === 0){ 

                head = current.next; // {1}

                // 如果只有一项,更新tail  //新增的
                if(length === 1){ // {2}
                    tail = null;
                }else{
                    head.prev = null; // {3}
                }
            }else if(pos === length - 1){ //最后一项 //新增的
                current = tail; // {4}
                tail = current.prev;
                tail.next = null;
            }else{
                while(index++ < pos){ // {5}
                    previous = current; 
                    current = current.next; 
                }
                // 将previous与current 的下一项链接起来:跳过current,从而移除它
                previous.next = current.next; // {6}
                current.next.prev = previous
            }
            length--; // {10}
            return current.element;
        }else{
            return null;
        }
    };

    // 从列表中移除一项
    this.remove = function(element){
        let index = this.indexOf(element);
        return this.removeAt(index);
    };

    // 返回元素在列表中的索引。如果列表中没有该元素则返回-1
    this.indexOf = function(element){
        let current = head,
            index = -1;
        while(current){
            if(element === current.element){
                return index;
            }
            index++;
            current = current.next
        }
        return -1;
    };

    // 如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false
    this.isEmpty = function(){
        return length === 0;
    };

    // 返回链表包含的元素个数
    this.size = function(){
        return length;
    };

    this.getHead = function(){
        return head;
    };

    // 由于列表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值
    this.toString = function(){
        let current = head,
            string = '',
            index = 0;
        while(current){
            string += current.element + (current.next ? `--第${index}个元素\n` : '');
            current = current.next;
            index++;
        }
        return `${string}--第${index}个元素`;
    };

}

集合

集合是一组无序且唯一的项组成的;在数学中,集合是一组不同的对象;空集是不包含任何元素的集合。可以把集合想像成没有重复元素、没有顺序概念的数组。

  • 创建集合
function Set(){
    let items = {};
    this.has = function(val){
        return val in items;
        // return items.items.hasOwnProperty(val)
    };
    this.add = function(val){
        if(!this.has(val)){
            items[val] = val;
            return true;
        }
        return false;
    };
    this.remove = function(val){
        if(this.has(val)){
            delete items[val];
            return true
        }
        return false;
    };
    this.clear = function(){
        items = {};
    };
    this.size = function(){
        return Object.keys(items).length;
        // 以下方法中的代码可以在任何浏览器中运行
        // let count = 0;
        // for(let key in items){
        //     if(items.hasOwnProperty(key)){
        //         ++count;
        //     }
        // }
        // return count;
    };
    this.values = function(){
        let values = [];
        for (let i=0, keys=Object.keys(items); i<keys.length; i++) {
            values.push(items[keys[i]]);
        }
        return values;
        // 以下方法中的代码可以在任何浏览器中运行
        // let values = [];
        // for(let key in items) { //{7}
        //     if(items.hasOwnProperty(key)) { //{8}
        //     values.push(items[key]);
        // } }
        // return values;
    }
}
  • 集合操作(并集、交集、差集、子集)
function Set(){
  
   ......
   
    // 并集
    this.union = function(otherSet){
        let unionSet = new Set();
        let values = this.values();
        for(let i=0; i<values.length; i++){
            unionSet.add(values[i])
        };
        
        values = otherSet.values();
        for(let i=0; i<values.length; i++){
            unionSet.add(values[i])
        }
        return unionSet.values();
    };
    // 交集
    this.intersection = function(otherSet){
        let intersection = new Set();

        let values = this.values();
        for(let i=0; i<values.length; i++){
            if(otherSet.has(values[i])){
                intersection.add(values[i])
            }
        }
        return intersection.values();
    };
    // 差集
    this.difference = function(otherSet){
        let differenceSet = new Set();

        let values = this.values();
        for(let i=0; i<values.length; i++){
            if(!otherSet.has(values[i])){
                differenceSet.add(values[i])
            }
        }
        return differenceSet.values();
    };
    // 子集
    this.subSet = function(otherSet){
        if(this.size() > otherSet.size()){
            return false;
        }else{
            let values = this.values();
            for(let i=0; i<values.length; i++){
                if(!otherSet.has(values[i])){
                    return false
                }
            }
            return true;
        }
    }
}

字典

字典和集合相似,集合以[值,值]的形式存储元素,字典则是以[键,值]的形式存储元素。字典也称作映射。

  • 创建字典
function Dictionary(){
    let items = {};
    // 如果某个键值存在于这个字典中,则返回true,反之则返回false。
    this.has = function(key){
        return key in items;
    };
    // 向字典中添加新元素
    this.set = function(key,val){
        items[key] = val;
    };
    // 通过使用键值来从字典中移除键值对应的数据值
    this.delete = function(key){
        if(items[key]){
            delete items[key];
            return true;
        }
        return false
    };
    // 通过键值查找特定的数值并返回。
    this.get = function(key){
        return this.has(key) ? items[key] : undefined;
    };
    // 将字典所包含的所有键名以数组形式返回。
    this.keys = function(){
        return Object.keys(items);
    };
    // 将字典所包含的所有数值以数组形式返回。
    this.values = function(){
        let values = [];
        for(let k in items){
            if(this.has(k)){
                values.push(items[k])
            }
        }
        return values;
    };
    this.clear = function(){
        items = {};
    };
    this.size = function(){
        return Object.keys(items).length;
    };
    this.getItems = function(){
        return items
    }
}

散列表

是Dictionary类的一种散列表实现方式,是一种非顺序数据结构。散列算法的作用是尽可能快地在数据结构中找到一个值。如果需要在数据结构中获取一个值,如果使用散列 函数,就知道值的具体位置,因此能够快速检索到该值。散列函数的作用是给定一个键值,然后 返回值在表中的地址。

  • 创建散列表
// 散列表
function HashTable(){
    let table = [];
    
    // 根据指定的key返回key中各个字母组成的ascii之和
    let hashCode = function(key){
        let hash = 0;
        for(let i=0; i<key.length; i++){
            hash += key.charCodeAt(i)
        }
        return hash % 37;
    }

    // 向散列表中添加新项(也能更新散列表)
    this.put = function(key,val){
        let pos = hashCode(key);
        console.log(pos+'-'+key);
        table[pos] = val;
    };
    // 根据键值从散列中移除值
    this.remove = function(key){
        table[hashCode(key)] = undefined;
    };
    // 根据键值检索特定的值
    this.get = function(key){
        return table[hashCode(key)]
    };
}

处理散列表中的冲突

  • 分离链接
// 解决冲突:分离链接
let LinkedList = require('../linkedList/LinkedList.js');
function HashTable(){
    let table = [];
    
    // 根据指定的key返回key中各个字母组成的ascii之和
    let hashCode = function(key){
        let hash = 0;
        for(let i=0; i<key.length; i++){
            hash += key.charCodeAt(i)
        }
        return hash % 37;
    }

    // 为了实现一个使用了分离链接的HashTable实例,我们需要一个新的辅助类来表示将要加入 LinkedList实例的元素
    let ValuePair = function(key,val){
        this.key = key;
        this.val = val;
        this.toString = function(){
            return '[' + this.key + ' - ' + this.val + ']';
        }
    }


    // 向散列表中添加新项(也能更新散列表)
    this.put = function(key,val){
        let pos = hashCode(key);
        if(table[pos] == undefined){
            table[pos] = new LinkedList();
        }
        table[pos].append( new ValuePair(key,val));
    };
    // 根据键值从散列中移除值
    this.remove = function(key){
        let pos = hashCode(key);
        if(table[pos] !== undefined){
            let current = table[pos].getHead();
            while(current.next){
                if(current.element.key === key){
                    table[pos].remove(current.element);
                    if(table[pos].isEmpty()){
                        table[pos] = undefined
                    }
                    return true;
                }
                current = current.next;
                 // 检查是否为第一个或最后一个元素
                if(current.element.key === key){
                    table[pos].remove(current.element);
                    if(table[pos].isEmpty()){
                        table[pos] = undefined;
                    }
                    return true
                }
            }
        }
        return false;
    };
    // 根据键值检索特定的值
    this.get = function(key){
        let pos = hashCode(key);
        if(table[pos] !== undefined){
            // 遍历链表来遍历键值
            let current = table[pos].getHead();

            while(current.next){
                if(current.element.key === key){  // ????????????
                    return current.element.val
                }
                current = current.next;
            }

            // 检查元素在链表第一个或最后一个节点的情况
            if(current.element.key === key){
                return current.element.val
            }
        }
        return undefined;
    };
    this.print = function(){
        for (var i = 0; i < table.length; ++i) { //{1}
          if (table[i] !== undefined) {        //{2}
                console.log(i + ": " + table[i]);//{3}
            } 
        }
    };
}

var hash = new HashTable();
hash.put('Gandalf', 'gandalf@email.com');
hash.put('John', 'johnsnow@email.com'); 
hash.put('Tyrion', 'tyrion@email.com');
hash.put('Aaron', 'aaron@email.com');
hash.put('Donnie', 'donnie@email.com');
hash.put('Ana', 'ana@email.com');
hash.put('Jonathan', 'jonathan@email.com');
hash.put('Jamie', 'jamie@email.com');
hash.put('Sue', 'sue@email.com');
hash.put('Mindy', 'mindy@email.com');
hash.put('Paul', 'paul@email.com');
hash.put('Nathan', 'nathan@email.com');
hash.print()
console.log(hash.remove('Sue'),'------------------------------------')
console.log(hash.get('Jonathan'));
  • 线性探查
// 解决冲突:线性探查
function HashTable(){
    let table = [];
    
    // 根据指定的key返回key中各个字母组成的ascii之和
    let hashCode = function(key){
        let hash = 0;
        for(let i=0; i<key.length; i++){
            hash += key.charCodeAt(i)
        }
        return hash % 37;
    }

    let ValuePair = function(key,val){
        this.key = key;
        this.val = val;
        this.toString = function(){
            // return [`${val}-${key}`]
            return '[' + this.key + ' - ' + this.val + ']';
        }
    }

    // 向散列表中添加新项(也能更新散列表)
    this.put = function(key, val){
        var pos = hashCode(key); // {1}
        if (table[pos] == undefined) { // {2}
            table[pos] = new ValuePair(key, val); // {3}
        } else {
            var index = ++pos; // {4}
            while (table[index] != undefined){ // {5}
                index++; // {6}
            }
            table[index] = new ValuePair(key, val); // {7}
        }
    };
    // 根据键值从散列中移除值
    this.remove = function(key){
        var pos = hashCode(key);
        if (table[pos] !== undefined){ //{8}
            if (table[pos].key === key) { //{9}
                return table[index] = undefined; //{10}
            } else {
                var index = ++pos;
                while (table[index] === undefined || table[index].key !== key){ //{11}
                    index++; 
                }
                if (table[index].key === key) { //{12}
                    return table[index] = undefined; //{13}
                } 
            }
        }
        return undefined; //{14}
    };
    // 根据键值检索特定的值
    this.get = function(key) {
        var pos = hashCode(key);
        if (table[pos] !== undefined){ //{8}
            if (table[pos].key === key) { //{9}
                return table[pos].val; //{10}
            } else {
                var index = ++pos;
                while (table[index] === undefined || table[index].key !== key){ //{11}
                    index++; 
                }
                if (table[index].key === key) { //{12}
                    return table[index].val; //{13}
                } 
            }
        }
        return undefined; //{14}
    };
    this.print = function(){
        for (var i = 0; i < table.length; ++i) { //{1}
          if (table[i] !== undefined) {        //{2}
                console.log(i + ": " + table[i]);//{3}
            } 
        }
    };
}

var hash = new HashTable();
hash.put('Gandalf', 'gandalf@email.com');
hash.put('John', 'johnsnow@email.com'); 
hash.put('Tyrion', 'tyrion@email.com');
hash.put('Aaron', 'aaron@email.com');
hash.put('Donnie', 'donnie@email.com');
hash.put('Ana', 'ana@email.com');
hash.put('Jonathan', 'jonathan@email.com');
hash.put('Jamie', 'jamie@email.com');
hash.put('Sue', 'sue@email.com');
hash.put('Mindy', 'mindy@email.com');
hash.put('Paul', 'paul@email.com');
hash.put('Nathan', 'nathan@email.com');
hash.print();
console.log(hash.get('Paul'))
hash.remove('Paul')
hash.print();

树是一种分层的抽象模型。现实生活中的例子有家谱或者公司的组织架构图。

二叉树

二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。

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

树的遍历

遍历一棵树是指访问树的每个节点并对它们进行某种操作的过程。

中序遍历


/**
 * 二叉搜索树
 * 键是树相关的术语中对节点的称呼
 * 和链表一样,将通过指针来表示节点之间的关系(术语称其为边)
 */
function BinarySearchTree(){

    // 声明一个Node来表示树中的每个节点
    let Node = function(key){
        this.key = key;
        this.left = null;
        this.right = null;
    }

    // 声明一个变量以控制此数据结构的第一个节点: 根元素
    let root = null;

  	// 辅助函数
    var insertNode = function(node, newNode){
        if(newNode.key < node.key){
            if(node.left === null){
                node.left = newNode;
            }else{
                insertNode(node.left, newNode);
            }
        }else{
            if(node.right === null){
                node.right = newNode;
            }else{
                insertNode(node.right, newNode)
            }
        }
    };

    var inOrderTraverseNode = function(node, callback){
        if(node !== null){
            inOrderTraverseNode(node.left, callback);
            callback(node.key);
            inOrderTraverseNode(node.right, callback);
        }
    };

    // 向树中插入一个新的键。
    this.insert = function(key){
        var newNode = new Node(key);
        if(root === null){
            root = newNode;
        }else{
            insertNode(root,newNode)
        }
    };

    // 通过中序遍历方式遍历所有节点(左根右)。
    this.inOrderTraverse = function(callback){
        inOrderTraverseNode(root, callback); 
    };
}

var tree = new BinarySearchTree();
tree.insert(11);
tree.insert(7);
tree.insert(15);
tree.insert(5);
tree.insert(3);
tree.insert(9);
tree.insert(8);
tree.insert(10);
tree.insert(13);
tree.insert(12);
tree.insert(14);
tree.insert(20);
tree.insert(18);
tree.insert(15);

tree.insert(6);

function printNode(value){
    console.log(value)
}

tree.inOrderTraverse(printNode);

先序遍历


/**
 * 二叉搜索树
 * 键是树相关的术语中对节点的称呼
 * 和链表一样,将通过指针来表示节点之间的关系(术语称其为边)
 */
function BinarySearchTree(){

		......
  
    var preOrderTraverseNode = function(node, callback){
        if(node !== null){
            callback(node.key);
            preOrderTraverseNode(node.left, callback);
            preOrderTraverseNode(node.right, callback)
        }
    }
  
  	......

    // 通过先序遍历方式遍历所有节点(根左右)。
    this.preOrderTraverse = function(callback){
        preOrderTraverseNode(root, callback);
    };
}

var tree = new BinarySearchTree();

......

function printNode(value){
    console.log(value)
}

tree.preOrderTraverse(printNode);

后序遍历

/**
 * 二叉搜索树
 * 键是树相关的术语中对节点的称呼
 * 和链表一样,将通过指针来表示节点之间的关系(术语称其为边)
 */
function BinarySearchTree(){

		......
  
     var postOrderTraverseNode = function(node, callback){
        if(node !== null){
            postOrderTraverseNode(node.left, callback);
            postOrderTraverseNode(node.right, callback)
            callback(node.key);
        }
    };
  
  	......

    // 通过先序遍历方式遍历所有节点(根左右)。
    this.postOrderTraverse = function(callback){
        postOrderTraverse(root, callback);
    };
}

var tree = new BinarySearchTree();

......

function printNode(value){
    console.log(value)
}

tree.postOrderTraverse(printNode);

搜索树中的值

搜索最大/最小值


/**
 * 二叉搜索树
 * 键是树相关的术语中对节点的称呼
 * 和链表一样,将通过指针来表示节点之间的关系(术语称其为边)
 */
function BinarySearchTree(){

   	......

    let minNode = function(node){
        if(node){
            while(node && node.left !== null){
                node = node.left;
            }
            return node.key
        }
        return null;
    };

    let maxNode = function(node){
        if(node){
            while(node && node.right !== null){
                node = node.right
            }
            return node.key
        }
        return null;
    }

    ......

    // 返回树中最小的值/键
    this.min = function(){
        return minNode(root);
    };

    // 返回树中最大的值/键
    this.max = function(){
        return maxNode(root);
    };

}

let tree = new BinarySearchTree();

......

console.log(tree.max(),'max'); //20 'max'
console.log(tree.min(),'min') //3 'min'

搜索一个特定的值

/**
 * 二叉搜索树
 * 键是树相关的术语中对节点的称呼
 * 和链表一样,将通过指针来表示节点之间的关系(术语称其为边)
 */
function BinarySearchTree(){

   	......

     let  searchNode = function(node, key){
        if(node === null){
            return false;
        }
        if(key < node.key){
            return searchNode(node.left, key)
        }else if(key < node.key){
            return searchNode(node.right, key)
        }else {
            return true;
        }
    };
    
    // 在树中查找一个键,如果节点存在,则返回true;如果不存在,则返回false。
    this.search = function(key){
        return searchNode(root, key)
    };

    ......

}

let tree = new BinarySearchTree();

......

console.log(tree.search(1)); // false
console.log(tree.search(8)); // true

移除一个节点


/**
 * 二叉搜索树
 * 键是树相关的术语中对节点的称呼
 * 和链表一样,将通过指针来表示节点之间的关系(术语称其为边)
 */
function BinarySearchTree(){

    ......

    let findMinNode = function(node){
        while(node && node.left !== null){
            node = node.left;
        }
        return node;
    };

    let removeNode = function(node, key){
        if(node === null){
            return null;
        }
        if(key < node.key){
            node.left = removeNode(node.left, key);
            return node;
        }else if(key > node.key){
            node.right = removeNode(node.right, key);
            return node;
        }else { // 键等于node.key

            // 第一种情况:一个叶节点
            if(node.left === null && node.right === null){
                node = null;
                return node;
            }

            // 第二种情况:一个只有一个子节点的节点
            if(node.left === null){
                node = node.right;
                return node;
            } else if(node.right === null) {
                node = node.left;
                return node;
            }

            //第三种情况:一个有两个子节点的节点
            let aux = findMinNode(node.right);
            node.key = aux.key;
            node.right = removeNode(node.right, aux.key);
            return node;
        }
    };

    ......

    // 从树中移除某个键
    this.remove = function(key){
        root = removeNode(root, key)
    }
}

let tree = new BinarySearchTree();

自平衡树

普通二叉搜索树可能出现一条分支有多层,而其他分支却只有几层的情况,这会导致添加、移除和搜索树具有性能问题。因此提出了自平衡二叉树的概念,AVL树(阿德尔森-维尔斯和兰迪斯树)是自平衡二叉树的一种,AVL树的任一子节点的左右两侧子树的高度之差不超过1,所以它也被称为高度平衡树。

深入了解:segmentfault.com/a/119000000…

红黑树

深入了解:

github.com/julycoding/…

www.cnblogs.com/skywang1234…

图是一种非线性数据结构,是网络结构的抽像模型,图是一组由边连接的节点,因为任何二元关系都可以用图来表示。

  • 基本概念
    • 由一条边连接在一起的顶点称为相邻顶点
    • 一个顶点的是其相邻顶点的数量。
    • 路径是顶点v1, v2,...,vk的一个连续序列
    • 简单路径要求不包含重复的顶点
    • 如果图中不存在环,则称图是无环的。如果图中每两个顶点都存在路径,则该图是连通的。
  • 有向图和无向图
    • 图可以是无向的(边没有方向)或是有向的(有向图),有向图的边只有一个方向;
    • 如果图中每两个顶点间都在双向上存在路径,则该图是强连通的
    • 图还可以是加权的或者未加权的, 加权的边被赋予了权值
  • 图的表示
    • 邻接矩阵
    • 邻接表
    • 关联矩阵
  • 代码实现
let Dictionary = require('../DictionaryAndHashTable/Dictionary'); // 引入 字典 数据结构

function Graph(){
    let vertices = []; //使用一个数组 来存储图中所有 顶点 的 名字
    let adjList = new Dictionary();  //用 字典 来存储邻接表。字典将会使用 顶点的名字 作为 键,邻接顶点列表 作为值。

    // 添加顶点
    this.addVertex = function(v){
        vertices.push(v);
        adjList.set(v, []);
    };

    // 添加边
    /**
     * 这个方法接受两个顶点作为参数。
     * 首先,通过将w加入到v的邻接表中,我们添加了一条自顶点v到顶点w的边。
     * 如果你想实现一个有向图,则行{5}就足够了。
     * 由于例子是基于无向图的,需要添加一条自w向v的边行{6}。
     *  */
    this.addEdge = function(v, w){
        adjList.get(v).push(w); //5
        adjList.get(w).push(v); //6
    };
    
    this.toString = function(){
        let s = '';
        for(let i=0; i<vertices.length; i++){
            s += vertices[i] + '--->';
            let neighbors = adjList.get(vertices[i]);
            for(let j = 0; j < neighbors.length; j++){
                s += neighbors[j] + ' ';
            }
            s += '\n';
        }
        return s;
    }
}

let graph = new Graph();
let myVertices = ['A','B','C','D','E','F','G','H','I'];

for(let i=0; i<myVertices.length; i++){
    graph.addVertex(myVertices[i]);
}
graph.addEdge('A', 'B'); //{9}
graph.addEdge('A', 'C'); 
graph.addEdge('A', 'D');
graph.addEdge('C', 'D');
graph.addEdge('C', 'G');
graph.addEdge('D', 'G');
graph.addEdge('D', 'H');
graph.addEdge('B', 'E');
graph.addEdge('B', 'F');
graph.addEdge('E', 'I');

console.log(graph.toString());

图的遍历

算法数据结构描述
深度优先搜索(Depth-First Search)通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相 邻顶点就去访问
广度优先搜索(Breadth-First Search)队列通过将顶点存入队列中,最先入队列的顶点先被探索

广度优先搜索

广度优先搜索算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访问图的一层。换句话说,就是先宽后深地访问顶点。

以下是从顶点v开始的广度优先搜索算法所遵循的步骤:

  1. 创建一个队列Q。
  2. 将v标注为被发现的(灰色),并将v入队列Q。
  3. 如果Q非空,则运行以下步骤:
    1. 将u从Q中出队列
    2. 将标注u为被发现的(灰色)
    3. 将u所有未被访问过的邻点(白色)入队列
    4. 将u标注为已被探索的(黑色)。
let Dictionary = require('../DictionaryAndHashTable/Dictionary');
let Queue = require('../queue/queue');

function Graph(){
 		......

    // 辅助函数
    // 广度优先和深度优先算法搜索都需要标注被访问过的顶点,算法开始执行时,所有的顶点颜色都是白色
    let initializeColor = function(){
        let color = [];
        for(let i=0; i<vertices.length; i++){
            color[vertices[i]] = 'white';
        }
        return color;
    };

    // 广度优先搜索
    this.bfs = function(v, callback){
        let color = initializeColor(); // 用此函数将数组初始化白色
        queue = new Queue(); // 此队列存储待访问和待探索的顶点
        queue.enqueue(v); // bfs方法接受一个顶点作为算法的起始点,将此顶点入队列

        /**
         * 如果队列非空,我们将通过出队列操作从队列中移除一个顶点,
         * 并取得一个包含其所有邻点的邻接表。该顶点将被标注为grey,
         * 表示我们发现了它(但还未完成对其的探索)。
         */
        while(!queue.isEmpty()){
            let u = queue.dequeue();
            neighbors = adjList.get(u);
            color[u] = 'grey';
            /**
             * 对于邻接表中每个邻点,我们取得其值(该顶点的名字),
             * 如果它还未被访问过(颜色为white),则将其标注为我们已经发现了它(颜色设置为grey),
             * 并将这个顶点加入队列中,这样当其从队列中出列的时候,我们可以完成对 其的探索。
             */
            for(let i=0; i<neighbors.length; i++){
                let w = neighbors[i];
                if(color[w] === 'white'){
                    color[w] = 'grey';
                    queue.enqueue(w);
                }
            }
            //当完成探索该顶点和其相邻顶点后,我们将该顶点标注为已探索过的(颜色设置为 black)。
            color[u] = 'black';
            if(callback){
                callback(u);
            }
        }
    }
}

let graph = new Graph();
let myVertices = ['A','B','C','D','E','F','G','H','I'];

......

console.log('广度优先搜索---------------');
function printNode(value){
    console.log(value)
}

graph.bfs(myVertices[0], printNode)
//广度优先搜索---------------
//A
//B
//C
//D
//E
//F
//G
//H
//I

使用BFS寻找最短路径

给定一个图G和源顶点v,找出对每个顶点u,u和v之间最短路径的距离(以边的数量计)。 对于给定顶点v,广度优先算法会访问所有与其距离为1的顶点,接着是距离为2的顶点, 以此类推。所以,可以用广度优先算法来解这个问题。我们可以修改bfs方法以返回给我们一 些信息:

  • 从v到u的距离d[u];
  •  前溯点pred[u],用来推导出从v到其他每个顶点u的最短路径。

下面是改进过的广度优先方法的实现:

let Dictionary = require('../DictionaryAndHashTable/Dictionary');
let Queue = require('../queue/queue');
let Stack = require('../stack/stack');

function Graph(){ 
    ...... 
    // 使用bfs寻找最短路径(改进版的bfs)
    this.BFS = function(v){
        let color = initializeColor();
        queue = new Queue();
        d = [], // 表示距离
        pred = [], // 表示前溯点
        queue.enqueue(v);

        for(let i=0; i<vertices.length; i++){
            d[vertices[i]] = 0;
            pred[vertices[i]] = null;
        }

        while(!queue.isEmpty()){
            let u = queue.dequeue(),
            neighbors = adjList.get(u);
            color[u] = 'grey';
            for(let j=0; j<neighbors.length; j++){
                let w = neighbors[j];
                if(color[w] === 'white'){
                    color[w] = 'grey';
                    d[w] = d[u] + 1;
                    pred[w] = u;
                    console.log('d:',d);
                    console.log('pred',pred)
                    queue.enqueue(w);
                }
            }
            color[u] = 'black';
        }
        return {
            distances: d,
            predecessors: pred
        }
    }
}

let graph = new Graph();
let myVertices = ['A','B','C','D','E','F','G','H','I'];
......
console.log('顶点A到其它顶点的路径(衡量标准是边的数量):')
// 我们用顶点A作为源顶点,对于每个其他顶点(除了顶点A),我们会计算顶点A到它的路径
let fromVertex = myVertices[0];
for(let i=1; i<myVertices.length; i++){
    // 我们从顶点数组得到toVertex
    let toVertex = myVertices[i],
    // 然后会创建一个栈来存储路径 值
    path = new Stack();
    // 追溯toVertex到fromVertex的路径,变量v被赋值为其前溯点的值, 这样我们能够反向追溯这条路径。
    for(let v=toVertex; v!==fromVertex; v=shortestPathA.predecessors[v]){
        path.push(v); // 将变量v添加到栈中
    }
    // 源顶点也会被添加到 栈中,以得到完整路径
    path.push(fromVertex);
    let s = path.pop();
    while(!path.isEmpty()){
        s += ' - ' + path.pop()
    }
    console.log(s);
}

深度优先搜索

深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶 点被访问了,接着原路回退并探索下一条路径。换句话说,它是先深度后广度地访问顶点。

let Dictionary = require('../DictionaryAndHashTable/Dictionary');
let Queue = require('../queue/queue');
let Stack = require('../stack/stack');

function Graph(){
   ......
   
    // 深度优先
    this.dfs = function(callback){
        // 我们创建颜色数组,并用值white为图中的每个顶点对其做初始化
        let color = initializeColor();

        // 对于图实例中每一个未被访问过的顶点,我们调用私有的递归函数dfsVisit,传递的参数为顶点、颜色数组以及回调函数
        for(let i=0; i<vertices.length; i++){
            if(color[vertices[i]] === "white"){
                dfsVisit(vertices[i], color, callback)
            }
        }
    };

    let dfsVisit = function(u, color, callback){
        // 当访问u顶点时,我们标注其为被发现的
        color[u] = 'grey';
        // 如果有callback函数的话,则执行该函数输出已访问过的顶点
        if(callback){
            callback(u)
        }
        // 取得包含顶点u所有邻点的列表
        let neighbors = adjList.get(u);
        // 对于顶点u的每一个未被访问过(颜色为white)的邻点w,我们将调用dfsVisit函数,接下来就能访问它
        for(let i=0; i<neighbors.length; i++){
            let w = neighbors[i];
            if(color[w] === 'white'){
                dfsVisit(w, color, callback)
            }
        }
        // 最后,在该顶点和邻点按深度访问之后,我们回退,意思是该顶点已被完全探索,并将其标注为black
        color[u] = 'black';
    }
}

let graph = new Graph();
let myVertices = ['A','B','C','D','E','F','G','H','I'];

......

console.log('深度优先搜索---------------');
graph.dfs(printNode)

排序算法

冒泡排序

比较任何两个相邻的项,如果第一个比第二个大,则交换它们。元素项向上移动至正确的顺序,就好像气泡升至表面一样,冒泡排序因此得名

改进后的冒泡排序:如果从内循环减去外循环中已跑过的轮数,就可以避免内循环中所有不必要的比较。

它的复杂度是O(n2)

this.bubbleSort = () => {
        let length = array.length

        for(let i = 0; i < length; i++){
            for(let j = 0; j < length - 1; j++){
            //冒泡改进版
            // for(let j = 0; j < length - 1 - i; j++){
                if(array[j]>array[j+1]){
                    [array[j],array[j+1]] = [array[j+1],array[j]]
                }
            }
        }
    }

选择排序

选择排序算法是一种原址比较排序算法。选择排序大致的思路是找到数据结构中的最小值并 将其放置在第一位,接着找到第二小的值并将其放在第二位,以此类推。

它的复杂度是O(n2)

 this.selectionSort = () => {
        let length = array.length,
            minIndex;
        for(let i = 0; i < length - 1; i++){
            minIndex = i; //假设本迭代轮次的第一个值为数组最小值
            for(let j = i; j < length; j++){
                if(array[minIndex] > array[j]){
                    minIndex = j
                }
            }
            if( i !== minIndex){
                [array[minIndex],array[i]] = [array[i],array[minIndex]]
            }
        }
    }

插入排序

插入排序每次排一个数组项,以此方式构建最后的排序数组。假定第一项已经排序了,接着, 它和第二项进行比较,第二项是应该待在原位还是插到第一项之前呢?这样,头两项就已正确排序,接着和第三项比较(它是该插入到第一、第二还是第三的位置呢?),以此类推。

 this.insertSort = () => {
        let length = array.length,
            j,
            temp;
        //注意,算法是从第二个位置(索引1)而不是0位置开始的(我们认为第一项已排序了)。
        for(let i = 1; i < length; i++){
            j = i;
            temp = array[i] 
            while( j > 0 && array[j-1] > temp){
                array[j] = array[j-1]
                j--
            }
            array[j] = temp
        }
    }

归并排序

归并排序是一种分治算法。其思想是将原始数组切分成较小的数组,直到每个小数组只有一 个位置,接着将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。

归并过程也会完成排序,直至原始数组完全合并并完成排序。

其复杂度为O(nlogn )。

快速排序

和归并排序一样,快速排序也使用分治的方法,将原始数组分为较小的数组(但它没有像归并排序那样将它们分割开)。 它的复杂度为O(nlogn )。

  • 首先,从数组中选择中间一项作为主元。
  • 创建两个指针,左边一个指向数组第一个项,右边一个指向数组最后一个项。移动左指 针直到我们找到一个比主元大的元素,接着,移动右指针直到找到一个比主元小的元素,然后交 换它们,重复这个过程,直到左指针超过了右指针。这个过程将使得比主元小的值都排在主元之 前,而比主元大的值都排在主元之后。这一步叫作划分操作。
  • 接着,算法对划分后的小数组(较主元小的值组成的子数组,以及较主元大的值组成的 子数组)重复之前的两个步骤,直至数组已完全排序。

堆排序

其把数组当作二叉树来排序。

  • 索引0是树的根节点
  • 任意节点N的左子节点是2*i+1
  • 任意节点N的右子节点是2*i+2

主要思想:

  • 构造一个满足array[parent(i)] ≥ array[i]的堆结构
  • 交换堆里第一个元素(数组中较大的值)和最后一个元素的位置。这样, 最大的值就会出现在它已排序的位置
  • 上一步骤可能会丢掉堆的属性。因此,我们还需要再次将数组转换成堆,(也就是说,它会找到当前堆的根节点(较小的值),重新放到树的底部)。直到堆中元素个数为1(或其对应数组的长度为1),排序完成。

计数排序

  • 概念

计数排序是一个非基于比较的排序算法。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(nlog(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(nlog(n)), 如归并排序,堆排序)

  • 主要思想

计数排序的基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。

  • 排序过程
    • 新建辅助数组,用来记录待排序数组中元素的出现频率,辅助数组下标为元素值、值为元素出现次数
    • 针对辅助数组进行遍历,重写待排序数组(或者返回一个新数组):因为辅助数组中的下标即为数组元素,根据数组下标的特性,对数组进行排序。

blog.csdn.net/an2766160/a…

桶排序

  • 概念

桶排序 (Bucket sort) 或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θn))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。

blog.csdn.net/an2766160/a…

基数排序

  • 概念

基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。

  • 主要思想

基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine)上的贡献。它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。

搜索算法

顺序搜索

顺序或线性搜索是最基本的搜索算法。它的机制是,将每一个数据结构中的元素和我们要找 的元素做比较。顺序搜索是最低效的一种搜索算法。

二分搜索

这个算法要求被搜索的数据结构已排序。

(1) 选择数组的中间值。

(2) 如果选中值是待搜索值,那么算法执行完毕(值找到了)。

(3) 如果待搜索值比选中值要小,则返回步骤1并在选中值左边的子数组中寻找。

(4) 如果待搜索值比选中值要大,则返回步骤1并在选种值右边的子数组中寻找。

注:参考JAVASCRIPT数据结构与算法第2版