导师计划--数据结构和算法系列(上)

structure-banner

导师计划已经开始一个月了,本身的讲解的课程选择了数据结构和算法。这个系列的讲解分为上下两章javascript语言辅助。本篇文章为上章,涉及的内容是基本的数据结构。在日本,晚上没事安排@…@,时间仍是充足的...,因而本身整理下本系列知识点的上章内容。javascript

moiunt-Fuji

如下为正文:前端

数据结构是计算机存储、组织数据的方式。数据结构是指相互直接存在一种或多种特殊关系的数据元素的集合。一般状况下,精心选择数据结构能够带来更高的运行或者存储效率。做为一名程序猿,更须要了解下数据结构。AND WHY?能够参考这篇文章【译】编程不容易中的性能和优化部份内容。java

讲到数据结构,咱们都会谈到线性结构和非线性结构。node

1.线性结构是一个有序数据元素的集合。它应该知足下面的特征:git

  • 集合中必存在惟一的一个“第一个元素”
  • 集合中必存在惟一的一个“最后的元素”
  • 除最后一元素以外,其它数据元素均有惟一的“后继”
  • 除第一个元素以外,其它数据元素均有惟一的“前驱”

按照百度百科的定义,咱们知道符合条件的数据结构就有栈、队列和其它。es6

2.非线性结构其逻辑特征是一个节点元素能够有多个直接前驱或多个直接后继。github

那么,符合条件的数据结构就有图、树和其它。算法

嗯~了解一下就行。咱们进入正题:编程

数组

数组是一种线性结构,以十二生肖(鼠、牛、虎、兔、龙、蛇、马、羊、猴、鸡、狗、猪)排序为例:数组

array_demo

咱们来建立一个数组并打印出结果就一目了然了:

let arr = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
arr.forEach((item, index) => {
	console.log(`[ ${index} ] => ${item}`);
});

// [ 0 ] => 鼠
// [ 1 ] => 牛
// [ 2 ] => 虎
// [ 3 ] => 兔
// [ 4 ] => 龙
// [ 5 ] => 蛇
// [ 6 ] => 马
// [ 7 ] => 羊
// [ 8 ] => 猴
// [ 9 ] => 鸡
// [ 10 ] => 狗
// [ 11 ] => 猪
复制代码

数组中经常使用的属性和一些方法以下,直接调用相关的方法便可。这里不作演示~

经常使用的属性

  • length : 表示数组的长度

经常使用的方法

  • splice(index, howmany, item, ... itemx)

    splice方法自认为是数组中最强大的方法。能够实现数组元素的添加、删除和替换。参数index为整数且必需,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置;参数howmany为必需,为要删除的项目数量,若是设置为 0,则不会删除项目;item1, ... itemx为可选,向数组添加新的项目。

  • indexOf(searchValue, fromIndex)

    indexOf方法返回某个指定字符串值在数组中的位置。searchValue是查询的字符串;fromIndex是查询的开始位置,默认是0。若是查询不到,会返回-1。

  • concat(array1, ... arrayn)

    concat方法用于链接两个或者多个数组。

  • push(newElement1, ... newElementN)

    push方法可向数组的末尾添加一个或者多个元素。

  • unshift(newElement1, ... newElementN)

    unshift方法可向数组的开头添加一个或者多个元素。

  • pop()

    pop方法用于删除并返回数组的最后一个元素

  • shift()

    shift方法能够删除数组的第一个元素

  • reverse()

    reverse方法用于数组的反转

  • sort(sortFn)

    sort方法是对数组的元素排序。参数sortFn可选,其规定排序顺序,必须是函数。

let values = [0, 1, 5, 10, 15];
values.sort();
console.log(values); // [0, 1, 10, 15, 5]
// 为何会出现这种排序结果呢❓
// 由于在忽略sortFn的状况下,元素会按照转换为字符串的各个字符的Unicode位点进行排序,以下
let equalValues = ['0', '1', '5', '10', '15'];
equalValues.sort();
console.log(equalValues); //  ["0", "1", "10", "15", "5"]

let arr = [0, 10, 5, 1, 15];
function compare(el1, el2){
    return el1 - el2; // 升序排列
}
arr.sort(compare);
console.log(arr); // [0, 1, 5, 10, 15]

arr.sort((el1, el2) => {
    return el2 - el1; // 降序排列
}); 
console.log(arr); // [15, 10, 5, 1, 0]
复制代码
  • forEach(fn(currentValue, index, arr), thisValue)

    forEach方法用于调用数组的每一个元素,并将元素传递给回调函数。参数function(currentValue, index, arr){}是一个回调函数。thisValue可选,传递给函数的值通常用 "this" 值,若是这个参数为空, "undefined" 会传递给 "this" 值。

  • every(fn(currentValue, index, arr), thisValue)

    every方法用于检测数组中全部元素是否符合指定条件,若是数组中检测到有一个元素不知足,则整个表达式返回false,且剩余的元素再也不检查。若是全部的元素都知足条件,则返回true

  • some(fn(currentValue,index,arr),thisValue)

    some方法用于检测数组中元素是否知足指定条件。只要有一个符合就返回true,剩余的元素再也不检查。若是全部元素都不符合条件,则返回false

  • reduce(fn(accumulator, currentValue, currentIndex, arr), initialValue)

    reduce方法接收一个函数做为累加器,数组中的每一个值(从左到右)开始缩减,最终为一个值。回调函数的四个参数的意义以下:accumulator,必需,累计器累计回调的返回值, 它是上一次调用回调时返回的累积值,或initialValue;currentValue,必需,数组中正在处理的元素;currentIndex,可选,数组中正在处理的当前元素的索引,若是提供了initialValue,则起始索引号为0,不然为1;arr,可选,当前元素所属的数组对象。initialValue,可选,传递给函数的初始值。

let arr = [1, 2, 3, 4];
let reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(arr.reduce(reducer)); // 10

// 5 + 1 + 2 + 3 + 4
console.log(arr.reduce(reducer, 5)); // 15
复制代码

是一种后进先出(LIFO)线性表,是一种基于数组的数据结构。(ps:其实后面讲到的数据结构或多或少有数组的影子)

  • LIFO(Last In First Out)表示后进先出,后进来的元素第一个弹出栈空间。相似于自动餐托盘,最后放上去的托盘,每每先被拿出来使用。
  • 仅容许在表的一端进行插入和移除元素。这一端被称为栈顶,相对地,把另外一端称为栈底。以下图的标识。
  • 向一个栈插入新元素称做进栈、入栈或压栈,这是将新元素放在栈顶元素上面,使之成为新的栈顶元素。
  • 从一个栈删除元素又称为出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

stack_demo

咱们代码写下,熟悉下栈:

class Stack {
    constructor(){
        this.items = [];
    }
    // 入栈操做
    push(element = ''){
        if(!element) return;
        this.items.push(element);
        return this;
    }
    // 出栈操做
    pop(){
        this.items.pop();
        return this;
    }
    // 对栈一瞥,理论上只能看到栈顶或者说即将处理的元素
    peek(){
        return this.items[this.size() - 1];
    }
    // 打印栈数据
    print(){
        return this.items.join(' ');
    }
    // 栈是否为空
    isEmpty(){
        return this.items.length == 0;
    }
    // 返回栈的元素个数
    size(){
        return this.items.length;
    }
}
let stack = new Stack(),
    arr = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
arr.forEach(item => {
    stack.push(item);
});
console.log(stack.print()); // 鼠 牛 虎 兔 龙 蛇 马 羊 猴 鸡 狗 猪
console.log(stack.peek()); // 猪
stack.pop().pop().pop().pop();
console.log(stack.print()); // 鼠 牛 虎 兔 龙 蛇 马 羊
console.log(stack.isEmpty()); // false
console.log(stack.size()); // 8
复制代码

⚠️ 注意:栈这里的push和pop方法要和数组方法的push和pop方法区分下。

说到,这也让我想到了翻译的一篇文章JS的执行上下文和环境栈是什么?,感兴趣的话能够戳进去看下。

队列

队列是一种先进先出(FIFO)受限的线性表。受限体如今于其容许在表的前端(front)进行删除操做,在表的末尾(rear)进行插入【优先队列这些排除在外】操做。

queue_demo

代码走一遍:

class Queue {
    constructor(){
        this.items = [];
    }
    // 入队操做
    enqueue(element = ''){
        if(!element) return;
        this.items.push(element);
        return this;
    }
    // 出队操做
    dequeue(){
        this.items.shift();
        return this;
    }
    // 查看队前元素或者说即将处理的元素
    front(){
        return this.items[0];
    }
    // 查看队列是否为空
    isEmpty(){
        return this.items.length == 0;
    }
    // 查看队列的长度
    len(){
        return this.items.length;
    }
    // 打印队列数据
    print(){
        return this.items.join(' ');
    }
}

let queue = new Queue(),
    arr = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
arr.forEach(item => {
    queue.enqueue(item);
});
console.log(queue.print()); // 鼠 牛 虎 兔 龙 蛇 马 羊 猴 鸡 狗 猪
console.log(queue.isEmpty()); // false
console.log(queue.len()); // 12
queue.dequeue().dequeue();
console.log(queue.front()); // 虎
console.log(queue.print()); // 虎 兔 龙 蛇 马 羊 猴 鸡 狗 猪
复制代码

链表

在进入正题以前,咱们先来聊聊数组的优缺点。

优势:

  • 存储多个元素,比较经常使用
  • 访问便捷,使用下标[index]便可访问

缺点:

  • 数组的建立一般须要申请一段连续的内存空间,而且大小是固定的(大多数的编程语言数组都是固定的),因此在进行扩容的时候难以掌控。(通常状况下,申请一个更大的数组,会是以前数组的倍数,好比两倍。而后,再将原数组中的元素复制过去)
  • 插入数据越是靠前,其成本很高,由于须要进行大量元素的位移。

相对数组,链表亦能够存储多个元素,并且存储的元素在内容中没必要是连续的空间;在插入和删除数据时,时间复杂度能够达到O(1)。在查找元素的时候,仍是须要从头开始遍历的,比数组在知道下表的状况下要快,可是数组若是不肯定下标的话,那就另说了...

咱们使用十二生肖来了解下链表:

linklist_demo

链表是由一组节点组成的集合。每一个节点都使用一个对象的引用指向它的后继。如上图。下面用代码实现下:

// 链表
class Node {
    constructor(element){
        this.element = element;
        this.next = null;
    }
}

class LinkedList {
    constructor(){
        this.length = 0; // 链表长度
        this.head = new Node('head'); // 表头节点
    }
    /** * @method find 查找元素的功能,找不到的状况下直接返回链尾节点 * @param { String } item 要查找的元素 * @return { Object } 返回查找到的节点 */
    find(item = ''){
        let currNode = this.head;
        while(currNode.element != item && currNode.next){
            currNode = currNode.next;
        }
        return currNode;
    }
    /** * @method findPrevious 查找链表指定元素的前一个节点 * @param { String } item 指定的元素 * @return { Object } 返回查找到的以前元素的前一个节点,找不到节点的话返回链尾节点 */
    findPrevious(item){
        let currNode = this.head;
        while((currNode.next != null) && (currNode.next.element != item)){
            currNode = currNode.next;
        }
        return currNode;
    }
    /** * @method insert 插入功能 * @param { String } newElement 要出入的元素 * @param { String } item 想要追加在后的元素(此元素不必定存在) */
    insert(newElement = '', item){
        if(!newElement) return;
        let newNode = new Node(newElement),
            currNode = this.find(item);
        newNode.next = currNode.next;
        currNode.next = newNode;
        this.length++;
        return this;
    }
    // 展现链表元素
    display(){
        let currNode = this.head,
            arr = [];
        while(currNode.next != null){
            arr.push(currNode.next.element);
            currNode = currNode.next;
        }
        return arr.join(' ');
    }
    // 链表的长度
    size(){
        return this.length;
    }
    // 查看链表是否为空
    isEmpty(){
        return this.length == 0;
    }
    /** * @method indexOf 查看链表中元素的索引 * @param { String } element 要查找的元素 */
    indexOf(element){
        let currNode = this.head,
            index = 0;
        while(currNode.next != null){
            index++;
            if(currNode.next.element == element){
                return index;
            }
            currNode = currNode.next;
        }
        return -1;
    }
    /** * @method removeEl 移除指定的元素 * @param { String } element */
    removeEl(element){
        let preNode = this.findPrevious(element);
        preNode.next = preNode.next != null ? preNode.next.next : null;
    }
}

let linkedlist = new LinkedList();
console.log(linkedlist.isEmpty()); // true
linkedlist.insert('鼠').insert('虎').insert('牛', '鼠');
console.log(linkedlist.display()); // 鼠 牛 虎
console.log(linkedlist.find('猪')); // Node { element: '虎', next: null }
console.log(linkedlist.find('鼠')); // Node { element: '鼠', next: Node { element: '牛', next: Node { element: '虎', next: null } } }
console.log(linkedlist.size()); // 3
console.log(linkedlist.indexOf('鼠')); // 1
console.log(linkedlist.indexOf('猪')); // -1
console.log(linkedlist.findPrevious('虎')); // Node { element: '牛', next: Node { element: '虎', next: null } }
linkedlist.removeEl('鼠');
console.log(linkedlist.display()); // 牛 虎
复制代码

字典

字典的主要特色是键值一一对应的关系。能够比喻成咱们现实学习中查不一样语言翻译的字典。这里字典的键(key)理论上是可使用任意的内容,但仍是建议语意化一点,好比下面的十二生肖图:

dictionary_demo

class Dictionary {
    constructor(){
        this.items = {};
    }
    /** * @method set 设置字典的键值对 * @param { String } key 键 * @param {*} value 值 */
    set(key = '', value = ''){
        this.items[key] = value;
        return this;
    }
    /** * @method get 获取某个值 * @param { String } key 键 */
    get(key = ''){
        return this.has(key) ? this.items[key] : undefined;
    }
    /** * @method has 判断是否含有某个键的值 * @param { String } key 键 */
    has(key = ''){
        return this.items.hasOwnProperty(key);
    }
    /** * @method remove 移除元素 * @param { String } key */
    remove(key){
        if(!this.has(key))  return false;
        delete this.items[key];
        return true;
    }
    // 展现字典的键
    keys(){
        return Object.keys(this.items).join(' ');
    }
    // 字典的大小
    size(){
        return Object.keys(this.items).length;
    }
    // 展现字典的值
    values(){
        return Object.values(this.items).join(' ');
    }
    // 清空字典
    clear(){
        this.items = {};
        return this;
    }
}

let dictionary = new Dictionary(),
    // 这里须要修改
    arr = [{ key: 'mouse', value: '鼠'}, {key: 'ox', value: '牛'}, {key: 'tiger', value: '虎'}, {key: 'rabbit', value: '兔'}, {key: 'dragon', value: '龙'}, {key: 'snake', value: '蛇'}, {key: 'horse', value: '马'}, {key: 'sheep', value: '羊'}, {key: 'monkey', value: '猴'}, {key: 'chicken', value: '鸡'}, {key: 'dog', value: '狗'}, {key: 'pig', value: '猪'}];
    arr.forEach(item => {
        dictionary.set(item.key, item.value);
    });
console.log(dictionary.keys()); // mouse ox tiger rabbit dragon snake horse sheep monkey chicken dog pig
console.log(dictionary.values()); // 鼠 牛 虎 兔 龙 蛇 马 羊 猴 鸡 狗 猪
console.log(dictionary.has('dragon')); // true
console.log(dictionary.get('tiger')); // 虎
console.log(dictionary.remove('pig')); // true
console.log(dictionary.size()); // 11
console.log(dictionary.clear().size()); // 0
复制代码

集合

集合一般是由一组无序的,不能重复的元素构成。 一些常见的集合操做如图:

set_demo

es6中已经封装好了可用的Set类。咱们手动来写下相关的逻辑:

// 集合
class Set {
    constructor(){
        this.items = [];
    }
    /** * @method add 添加元素 * @param { String } element * @return { Boolean } */
    add(element = ''){
        if(this.items.indexOf(element) >= 0) return false;
        this.items.push(element);
        return true;
    }
    // 集合的大小
    size(){
        return this.items.length;
    }
    // 集合是否包含某指定元素
    has(element = ''){
        return this.items.indexOf(element) >= 0;
    }
    // 展现集合
    show(){
        return this.items.join(' ');
    }
    // 移除某个元素
    remove(element){
        let pos = this.items.indexOf(element);
        if(pos < 0) return false;
        this.items.splice(pos, 1);
        return true;
    }
    /** * @method union 并集 * @param { Array } set 数组集合 * @return { Object } 返回并集的对象 */
    union(set = []){
        let tempSet = new Set();
        for(let i = 0; i < this.items.length; i++){
            tempSet.add(this.items[i]);
        }
        for(let i = 0; i < set.items.length; i++){
            if(tempSet.has(set.items[i])) continue;
            tempSet.items.push(set.items[i]);
        }
        return tempSet;
    }
    /** * @method intersect 交集 * @param { Array } set 数组集合 * @return { Object } 返回交集的对象 */
    intersect(set = []){
        let tempSet = new Set();
        for(let i = 0; i < this.items.length; i++){
            if(set.has(this.items[i])){
                tempSet.add(this.items[i]);
            }
        }
        return tempSet;
    }
    /** * @method isSubsetOf 【A】是【B】的子集❓ * @param { Array } set 数组集合 * @return { Boolean } 返回真假值 */
    isSubsetOf(set = []){
        if(this.size() > set.size()) return false;
        this.items.forEach*(item => {
            if(!set.has(item)) return false;
        });
        return true;
    }
}

let set = new Set(),
    arr = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴'];
arr.forEach(item => {
    set.add(item);
});
console.log(set.show()); // 鼠 牛 虎 兔 龙 蛇 马 羊 猴
console.log(set.has('猪')); // false
console.log(set.size()); // 9
set.remove('鼠');
console.log(set.show()); // 牛 虎 兔 龙 蛇 马 羊 猴
let setAnother = new Set(),
    anotherArr = ['马', '羊', '猴', '鸡', '狗', '猪'];
anotherArr.forEach(item => {
    setAnother.add(item);
});
console.log(set.union(setAnother).show()); // 牛 虎 兔 龙 蛇 马 羊 猴 鸡 狗 猪
console.log(set.intersect(setAnother).show()); // 马 羊 猴
console.log(set.isSubsetOf(setAnother)); // false
复制代码

散列表/哈希表

散列是一种经常使用的存储技术,散列使用的数据结构叫作散列表/哈希表。在散列表上插入、删除和取用数据都很是快,可是对于查找操做来讲却效率低下,好比查找一组数据中的最大值和最小值。查找的这些操做得求助其它数据结构,好比下面要讲的二叉树。

切入个案例感觉下哈希表:

假如一家公司有1000个员工, 如今咱们须要将这些员工的信息使用某种数据结构来保存起来。你会采用什么数据结构呢?

  • 方案一:数组

    • 按照顺序将全部员工信息依次存入一个长度为1000的数组中。每一个员工的信息都保存在该数组的某个位置上。
    • 可是咱们要查看某个员工的信息怎么办呢?一个个查找吗?不太好找。
    • 数组最大的优点是什么?经过下标值获取信息。
    • 因此为了能够经过数组快速定位到某个员工,最好给员工信息中添加一个员工编号,而编号对应的就是员工的下标值
    • 当查找某个员工信息时,经过员工号能够快速定位到员工的信息位置。
  • 方案二:链表

    • 链表对应插入和删除数据有必定的优点。
    • 可是对于获取员工的信息,每次都必须从头遍历到尾,这种方式显然不是特别适合咱们这里。
  • 最终方案:

    • 这么看最终方案彷佛就是数组了,可是数组仍是有缺点,什么缺点呢?
    • 假如咱们想查看下张三这位员工的信息,可是咱们不知道张三的员工编号,怎么办呢?
    • 固然,咱们能够问他的员工编号。可是咱们每查找一个员工都是要问一下这个员工的编号吗?不合适。【那咱们还不如直接问他的信息嘞】
    • 能不能有一种办法,让张三的名字和他的员工编号产生直接的关系呢?
    • 也就是经过张三这个名字,咱们就能获取到他的索引值,而再经过索引值咱们就能获取张三的信息呢?
    • 这样的方案已经存在了,就是使用哈希函数,让某个key的信息和索引值对应起来。

那么散列表的原理和实现又是怎样的呢,咱们来聊聊。

咱们的哈希表是基于数组完成的,咱们从数组这里切入解析下。数组能够经过下标直接定位到相应的空间,哈希表的作法就是相似的实现。哈希表把key(键)经过一个固定的算法函数(此函数称为哈希函数/散列函数)转换成一个整型数字,而后就将该数字对数组长度进行取余,取余结果就看成数组的下标,将value(值)存储在以该数字为下标的数组空间里,而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value

结合下面的代码,也许你会更容易理解:

// 哈希表
class HashTable {
    constructor(){
        this.table = new Array(137);
    }
    /** * @method hashFn 哈希函数 * @param { String } data 传入的字符串 * @return { Number } 返回取余的数字 */
    hashFn(data){
        let total = 0;
        for(let i = 0; i < data.length; i++){
            total += data.charCodeAt(i);
        }
        return total % this.table.length;
    }
    /** * * @param { String } data 传入的字符串 */
    put(data){
        let pos = this.hashFn(data);
        this.table[pos] = data;
        return this;
    }
    // 展现
    show(){
        this.table && this.table.forEach((item, index) => {
            if(item != undefined){
                console.log(index + ' => ' + item);
            }
        })
    }
    // ...获取值get函数等看官感兴趣的话本身补充测试啦
}

let hashtable = new HashTable(),
    arr = ['mouse', 'ox', 'tiger', 'rabbit', 'dragon', 'snake', 'horse', 'sheep', 'monkey', 'chicken', 'dog', 'pig'];
arr.forEach(item => {
    hashtable.put(item);
});
hashtable.show();
// 5 => mouse
// 40 => dog
// 46 => pig
// 80 => rabbit
// 87 => dragon
// 94 => ox
// 111 => monkey
// 119 => snake
// 122 => sheep
// 128 => tiger
// 134 => horse

// 那么问题来了,十二生肖里面的_小鸡_去哪里了呢❓
// 被_小萌狗_给覆盖了,由于其位置都是40(这个能够本身证实下)
// 问题又来了,那么应该如何解决这种被覆盖的冲突呢❓
复制代码

hashtable_demo

针对上面的问题,咱们存储数据的时候,产生冲突的话咱们能够像下面这样解决:

1. 线性探测法

当发生碰撞(冲突)时,线性探测法检查散列表中的下一个位置【有可能非顺序查找位置,不必定是下一个位置】是否为空。若是为空,就将数据存入该位置;若是不为空,则继续检查下一个位置,直到找到一个空的位置为止。该技术是基于一个事实:每一个散列表都有不少空的单元格,可使用它们存储数据。

2. 开链法

可是,当发生碰撞时,咱们任然但愿将key(键)存储到经过哈希函数产生的索引位置上,那么咱们可使用开链法开链法是指实现哈希表底层的数组中,每一个数组元素又是一个新的数据结构,好比另外一个数组(这样结合起来就是二位数组了),链表等,这样就能存储多个键了。使用这种技术,即便两个key(键)散列后的值相同,依然是被保存在一样的位置,只不过它们是被保存在另外一个数据结构上而已。以另外一个数据结构是数组为例,存储的数据以下:

open_link_method

二叉查找树

  • 树的定义:

    • 树(Tree):n(n >= 0)个节点构成的有限集合。

      • n = 0时,称为空树;
      • 对任意一棵空树(n > 0),它具有如下性质:
      • 树中有一个称为**根(Root)**的特殊节点,用r(root)表示;
      • 其他节点可分为m(m > 0)个互不相交的有限集T1,T2,...Tm,其中每一个集合本省又是一棵树,称为原来树的子树(SubTree)
    • 注意:

      • 子树之间不能够相交
      • 除了根节点外,每一个节点有且仅有一个父节点;
      • 一个N个节点的树有N-1条边。
  • 树的术语:

    • 节点的度(Degree):节点的子树个数。
    • 树的度:树的全部节点中最大的度数(树的度一般为节点个数的N-1)。
    • 叶节点(Leaf):度为0的节点(也称叶子节点)。
    • 父节点(Parent):有子树的节点是其子树的父节点。
    • 子节点(Child):若A节点是B节点的父节点,则称B节点是A节点的子节点。
    • 兄弟节点(Sibling):具备同一个父节点的各节点彼此是兄弟节点。
    • 路径和路径长度:从节点n1nk的路径为一个节点序列n1,n2,n3,...,nknini+1的父节点。路径所包含边的个数为路径长度。
    • 节点的层次(Level):规定根节点在第0层,它的子节点是第1层,子节点的子节点是第2层,以此类推。
    • 树的深度(Depth):树中全部节点中的最大层次是这棵树的深度(由于上面是从第0层开始,深度 = 第最大层数 + 1)

以下图:

tree_intro

  • 二叉树的定义:

    • 二叉树能够为空,也就是没有节点
    • 二叉树若不为空,则它是由根节点和称为其左子树TL和右子树RT的两个不相交的二叉树组成
    • 二叉树每一个节点的子节点不容许超过两个
  • 二叉树的五种形态:

    • 只有根节点
    • 只有左子树
    • 只有右子树
    • 左右子树均有

对应下图(从左至右):

five_style_binary_tree

咱们接下来要讲的是二叉查找树(BST,Binary Search Tree)二叉查找树,也称二叉搜索树或二叉排序树,是一种特殊的二叉树,相对值较的值保存在节点中,较的值保存在节点中。二叉查找树特殊的结构使它可以快速的进行查找、插入和删除数据。下面咱们来实现下:

// 二叉查找树
// 辅助节点类
class Node {
    constructor(data, left, right){
        this.data = data;
        this.left = left;
        this.right = right;
    }
    // 展现节点信息
    show(){
        return this.data;
    }
}
class BST {
    constructor(){
        this.root = null;
    }
    // 插入数据
    insert(data){
        let n = new Node(data, null, null);
        if(this.root == null){
            this.root = n;
        }else{
            let current = this.root,
                parent = null;
            while(true){
                parent = current;
                if(data < current.data){
                    current = current.left;
                    if(current == null){
                        parent.left = n;
                        break;
                    }
                }else{
                    current = current.right;
                    if(current == null){
                        parent.right = n;
                        break;
                    }
                }
            }
        }
        return this;
    }
    // 中序遍历
    inOrder(node){
        if(!(node == null)){
            this.inOrder(node.left);
            console.log(node.show());
            this.inOrder(node.right);
        }
    }
    // 先序遍历
    preOrder(node){
        if(!(node == null)){
            console.log(node.show());
            this.preOrder(node.left);
            this.preOrder(node.right);
        }
    }
    // 后序遍历
    postOrder(node){
        if(!(node == null)){
            this.postOrder(node.left);
            this.postOrder(node.right);
            console.log(node.show());
        }
    }
    // 获取最小值
    getMin(){
        let current = this.root;
        while(!(current.left == null)){
            current = current.left;
        }
        return current.data;
    }
    // 获取最大值
    getMax(){
        let current = this.root;
        while(!(current.right == null)){
            current = current.right;
        }
        return current.data;
    }
    // 查找给定的值
    find(data){
        let current = this.root;
        while(current != null){
            if(current.data == data){
                return current;
            }else if(data < current.data){
                current = current.left;
            }else{
                current = current.right;
            }
        }
        return null;
    }
    // 移除给定的值
    remove(data){
        root = this.removeNode(this.root, data);
        return this;
    }
    // 移除给定值的辅助函数
    removeNode(node, data){
        if(node == null){
            return null;
        }
        if(data == node.data){
            // 叶子节点
            if(node.left == null && node.right == null){
                return null; // 此节点置空
            }
            // 没有左子树
            if(node.left == null){
                return node.right;
            }
            // 没有右子树
            if(node.right == null){
                return node.left;
            }
            // 有两个子节点的状况
            let tempNode = this.getSmallest(node.right); // 获取右子树
            node.data = tempNode.data; // 将其右子树的最小值赋值给删除的那个节点值
            node.right = this.removeNode(node.right, tempNode.data); // 删除指定节点的下的最小值,也就是置其为空
            return node;
        }else if(data < node.data){
            node.left = this.removeNode(node.left, data);
            return node;
        }else{
            node.right = this.removeNode(node.right, data);
            return node;
        }
    }
    // 获取给定节点下的二叉树最小值的辅助函数
    getSmallest(node){
        if(node.left == null){
            return node;
        }else{
            return this.getSmallest(node.left);
        }
    }
}

let bst = new BST();
bst.insert(56).insert(22).insert(10).insert(30).insert(81).insert(77).insert(92);
bst.inOrder(bst.root); // 10, 22, 30, 56, 77, 81, 92
console.log('--中序和先序遍历分割线--');
bst.preOrder(bst.root); // 56, 22, 10, 30, 81, 77, 92
console.log('--先序和后序遍历分割线--');
bst.postOrder(bst.root); // 10, 30, 22, 77, 92, 81, 56
console.log('--后序遍历和获取最小值分割线--');
console.log(bst.getMin()); // 10
console.log(bst.getMax()); // 92
console.log(bst.find(22)); // Node { data: 22, left: Node { data: 10, left: null, right: null }, right: Node { data: 30, left: null, right: null } }
// 咱们删除节点值为22,而后用先序的方法遍历,以下
console.log('--移除22的分割线--')
console.log(bst.remove(22).inOrder(bst.root)); // 10, 30, 56, 77, 81, 92
复制代码

看了上面的代码以后,你是否有些懵圈呢?咱们借助几张图来了解下,或许你就豁然开朗了。

在遍历的时候,咱们分为三种遍历方法--先序遍历,中序遍历和后序遍历:

travel_tree

删除节点是一个比较复杂的操做,考虑的状况比较多:

  • 该节点没有叶子节点的时候,直接将该节点置空;
  • 该节点只有左子树,直接将该节点赋予左子树
  • 该节点只有右子树,直接将该节点赋予右子树
  • 该节点左右子树都有,有两种方法能够处理
    • 方案一:从待删除节点的子树找节点值最大的节点A,替换待删除节点值,并删除节点A
    • 方案二:从待删除节点的子树找节点值最小的节点A,替换待删除节点值,并删除节点A【👆上面的示例代码中就是这种方案】

删除两个节点的图解以下:

remove_tree_node

由边的集合及顶点的集合组成。

咱们来了解下图的相关术语:

  • 顶点:图中的一个节点。
  • 边:表示顶点和顶点之间的连线。
  • 相邻顶点:由一条边链接在一块儿的顶点称为相邻顶点。
  • 度:一个顶点的度是相邻顶点的数量。好比0顶点和其它两个顶点相连,0顶点的度就是2
  • 路径:路径是顶点v1,v2...,vn的一个连续序列。
    • 简单路径:简单路径要求不包含重复的顶点。
    • 回路:第一个顶点和最后一个顶点相同的路径称为回路。
  • 有向图和无向图
    • 有向图表示图中的方向的。
    • 无向图表示图中的方向的。
  • 带权图和无权图
    • 带权图表示图中的边有权重
    • 无权图表示图中的边无权重

以下图:

graph_concept_intro

图能够用于现实中的不少系统建模,好比:

  • 对交通流量建模
    • 顶点能够表示街道的十字路口, 边能够表示街道.
    • 加权的边能够表示限速或者车道的数量或者街道的距离.
    • 建模人员能够用这个系统来断定最佳路线以及最可能堵车的街道.

图既然这么方便,咱们来用代码实现下:

// 图
class Graph{
    constructor(v){
        this.vertices = v; // 顶点个数
        this.edges = 0; // 边的个数
        this.adj = []; // 邻接表或邻接表数组
        this.marked = []; // 存储顶点是否被访问过的标识
        this.init();
    }
    init(){
        for(let i = 0; i < this.vertices; i++){
            this.adj[i] = [];
            this.marked[i] = false;
        }
    }
    // 添加边
    addEdge(v, w){
        this.adj[v].push(w);
        this.adj[w].push(v);
        this.edges++;
        return this;
    }
    // 展现图
    showGraph(){
        for(let i = 0; i < this.vertices; i++){
            for(let j = 0; j < this.vertices; j++){
                if(this.adj[i][j] != undefined){
                    console.log(i +' => ' + this.adj[i][j]);
                }
            }
        }
    }
    // 深度优先搜索
    dfs(v){
        this.marked[v] = true;
        if(this.adj[v] != undefined){
            console.log("visited vertex: " + v);
        }
        this.adj[v].forEach(w => {
            if(!this.marked[w]){
                this.dfs(w);
            }
        })
    }
    // 广度优先搜索
    bfs(v){
        let queue = [];
        this.marked[v] = true;
        queue.push(v); // 添加到队尾
        while(queue.length > 0){
            let v = queue.shift(); // 从对首移除
            if(v != undefined){
                console.log("visited vertex: " + v);
            }
            this.adj[v].forEach(w => {
                if(!this.marked[w]){
                    this.marked[w] = true;
                    queue.push(w);
                }
            })
        }
    }
}

let graphFirstInstance = new Graph(5);
graphFirstInstance.addEdge(0, 1).addEdge(0, 2).addEdge(1, 3).addEdge(2, 4);
graphFirstInstance.showGraph();
// 0 => 1
// 0 => 2
// 1 => 0
// 1 => 3
// 2 => 0
// 2 => 4
// 3 => 1
// 4 => 2
// ❓为何会出现这种数据呢?它对应的图是什么呢?能够思考🤔下,动手画画图什么的
console.log('--展现图和深度优先搜索的分隔线--');
graphFirstInstance.dfs(0); // 从顶点 0 开始的深度搜索
// visited vertex: 0
// visited vertex: 1
// visited vertex: 3
// visited vertex: 2
// visited vertex: 4
console.log('--深度优先搜索和广度优先搜索的分隔线--');
let graphSecondInstance = new Graph(5);
graphSecondInstance.addEdge(0, 1).addEdge(0, 2).addEdge(1, 3).addEdge(2, 4);
graphSecondInstance.bfs(0); // 从顶点 0 开始的广度搜索
// visited vertex: 0
// visited vertex: 1
// visited vertex: 2
// visited vertex: 3
// visited vertex: 4
复制代码

对于搜索图,在上面咱们介绍了深度优先搜索 - DFS(Depth First Search)广度优先搜索 - BFS(Breadth First Search),结合下面的图再回头看下上面的代码,你会更加容易理解这两种搜索图的方式。

graph_search

后话

文章中的一些案例来自coderwhy的数据结构和算法系列文章,感谢其受权

author_wechat_permission

绘图软件 Numbers,本篇文章用到的图片绘图稿感兴趣能够下载。

演示代码存放地址 -- 数据结构文件夹 进入structure目录能够直接 node + filename 运行

文章首发 github.com/reng99/blog…

更多内容 github.com/reng99/blog…

参考

相关文章
相关标签/搜索