JavaScript中常见数据结构

数据结构

  • :一种遵从先进后出 (LIFO) 原则的有序集合;新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端为栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。
  • 队列:与上相反,一种遵循先进先出 (FIFO / First In First Out) 原则的一组有序的项;队列在尾部添加新元素,并从头部移除元素。最新添加的元素必须排在队列的末尾。
  • 链表:存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的;每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(指针/链接)组成。
  • 集合:由一组无序且唯一(即不能重复)的项组成;这个数据结构使用了与有限集合相同的数学概念,但应用在计算机科学的数据结构中。
  • 字典:以 [键,值] 对为数据形态的数据结构,其中键名用来查询特定元素,类似于 Javascript 中的Object
  • 散列:根据关键码值(Key value)直接进行访问的数据结构;它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度;这个映射函数叫做散列函数,存放记录的数组叫做散列表。
  • :由 n(n>=1)个有限节点组成一个具有层次关系的集合;把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的,基本呈一对多关系,树也可以看做是图的特殊形式。
  • :图是网络结构的抽象模型;图是一组由边连接的节点(顶点);任何二元关系都可以用图来表示,常见的比如:道路图、关系图,呈多对多关系。

 

 

生活中常见的Stack的例子比如一摞书,你最后放上去的那本你之后会最先拿走;又比如浏览器的访问历史,当点击返回按钮,最后访问的网站最先从历史记录中弹出。

Stack一般具备以下方法:

  1. push:将一个元素推入栈顶
  2. pop:移除栈顶元素,并返回被移除的元素
  3. peek:返回栈顶元素
  4. isEmpty(): 栈中是否有元素
  5. clear(): 移除栈中所有元素
  6. size:返回栈中元素的个数

以对象形式实现栈

    <script type="text/javascript">
        function Stack() {
            this.count = 0;
            this.storage = {};
            //将一个元素推入栈顶
            this.push = function (value) {
                this.storage[this.count] = value;
                this.count++;
            }
            //移除栈顶元素,并返回被移除的元素
            this.pop = function () {
                if (this.count === 0) {
                    return undefined;
                }
                this.count--;
                var result = this.storage[this.count];
                delete this.storage[this.count];
                return result;
            }
            //返回栈顶元素
            this.peek = function () {
                return this.storage[this.count - 1];
            }
            //栈中是否有元素
            this.isEmpty = function () {
                //使用es6语法判断对象中属性长度
                //return Object.keys(this.storage).length==0;
                return this.count==0;
            }
            //移除栈中所有元素
            this.clear = function () {
                this.count = 0
                //return this.storage={};
            }
            //返回栈中元素的个数
            this.size = function () {
                return this.count;
            }
        }


        var newStack = new Stack();
        newStack.push("第一个元素");
        newStack.push("第二个元素");
        newStack.push("第三个元素");
        console.log("打印栈中元素个数:" + newStack.size());
        console.log("打印栈中栈顶元素:" + newStack.peek());
        console.log("打印栈中移除元素:" + newStack.pop());
        console.log("移除栈顶元素后再次打印栈中栈顶元素:" + newStack.peek());
        console.log("判断栈中是否有元素:" + newStack.isEmpty());
        console.log("移除栈中所有元素:" + newStack.clear());
        console.log("移除后判断栈中是否有元素:" + newStack.isEmpty());
        console.log("打印栈中移除元素:" + newStack.pop());
    </script>

以数组形式实现栈

    <script type="text/javascript">
        function Stack() {
            //保存栈内元素的数组
            this.dataStore = [];
            //top用来记录栈顶位置,初始化为0        
            this.top = 0;

            this.push = function (element) {
                this.dataStore[this.top++] = element;   // 先在top位置加入元素,之后top加1
            }
            this.pop = function () {
                // top先减1,然后返回top位置的元素
                return this.dataStore[--this.top];
            }
            this.peek = function peek() {
                return this.dataStore[this.top - 1];
            }

            
            this.isEmpty = function clear() {
                return this.top ==0;
            }

            this.clear = function clear() {
                this.top = 0;
            }

            this.length = function length() {
                return this.top;
            }
        }
        var newStack = new Stack();
        newStack.push("第一个元素");
        newStack.push("第二个元素");
        newStack.push("第三个元素");
        console.log("打印栈中元素个数:" + newStack.length());
        console.log("打印栈中栈顶元素:" + newStack.peek());
        console.log("打印栈中移除元素:" + newStack.pop());
        console.log("移除栈顶元素后再次打印栈中栈顶元素:" + newStack.peek());
        console.log("判断栈中是否有元素:" + newStack.isEmpty());
        console.log("移除栈中所有元素:" + newStack.clear());
        console.log("移除后判断栈中是否有元素:" + newStack.isEmpty());
    </script>

 Queue(队列)

 

Queue和Stack有一些类似,不同的是Stack是先进后出,而Queue是先进先出。Queue在生活中的例子比如排队上公交,排在第一个的总是最先上车;又比如打印机的打印队列,排在前面的最先打印。

Queue一般具有以下常见方法:

  1. enqueue:入列,向队列尾部增加一个元素
  2. dequeue:出列,移除队列头部的一个元素并返回被移除的元素
  3. front:获取队列的第一个元素
  4. isEmpty:判断队列是否为空
  5. size:获取队列中元素的个数

Javascript中的Array已经具备了Queue的一些特性,所以我们可以借助Array实现一个Queue类型:

    <script type="text/javascript">
        function Queue() {
            var collection = [];
            //返回队列所有元素
            this.print = function () {
                return collection;
            }
            //入列,向队列尾部增加一个元素
            this.enqueue = function (element) {
                collection.push(element);
            }
            //出列,移除队列头部的一个元素并返回被移除的元素
            this.dequeue = function () {
                return collection.shift();
            }
            //获取队列的第一个元素
            this.front = function () {
                return collection[0];
            }
            //判断队列是否为空
            this.isEmpty = function () {
                return collection.length === 0;
            }
            //清除队列
            this.clear = function () {
                items = [];
            };
            //获取队列中元素的个数
            this.size = function () {
                return collection.length;
            }
        }
        var newQueue= new Queue();
        newQueue.enqueue("第一个元素");
        newQueue.enqueue("第二个元素");
        newQueue.enqueue("第三个元素");
        console.log("打印队列中元素个数:" + newQueue.size());
        console.log("打印队列中第一个元素:" + newQueue.front());
        console.log("打印队列中所有元素:" + newQueue.print());
        console.log("打印队列中移除元素:" + newQueue.dequeue());
        console.log("移除队列元素后再次打印队列中所有元素:" + newQueue.print());
        console.log("判断队列中是否有元素:" + newQueue.isEmpty());
        console.log("移除队列中所有元素:" + newQueue.clear());
        console.log("移除后判断队列中是否有元素:" + newQueue.isEmpty());
    </script>

 Priority Queue(优先队列)

Queue还有个升级版本,给每个元素赋予优先级,优先级高的元素入列时将排到低优先级元素之前。区别主要是enqueue方法的实现:

    <script type="text/javascript">
        function PriorityQueue() {
            var collection = [];
            //返回队列所有元素
            this.print = function () {
                return collection;
            }
            //入列,向队列尾部增加一个元素
            this.enqueue = function (element) {
                collection.push(element);
            }
            //出列,移除队列头部的一个元素并返回被移除的元素
            this.dequeue = function () {
                return collection.shift();
            }
            //获取队列的第一个元素
            this.front = function () {
                return collection[0];
            }
            //判断队列是否为空
            this.isEmpty = function () {
                return collection.length === 0;
            }
            //清除队列
            this.clear = function () {
                items = [];
            };
            //获取队列中元素的个数
            this.size = function () {
                return collection.length;
            }
            //优先队列
            this.priorityEnqueue = function (element) {
                if (this.isEmpty()) {
                    collection.push(element);
                } else {
                    var added = false;
                    for (var i = 0; i < collection.length; i++) {
                        if (element[1] < collection[i][1]) {
                            collection.splice(i, 0, element);
                            added = true;
                            break;
                        }
                    }
                    if (!added) {
                        collection.push(element);
                    }
                }
            }
        }
        var newQueue = new PriorityQueue();
        newQueue.enqueue("第一个元素");
        newQueue.enqueue("第二个元素");
        newQueue.enqueue("第三个元素");
        console.log("打印队列中元素个数:" + newQueue.size());
        console.log("打印队列中第一个元素:" + newQueue.front());
        console.log("打印队列中所有元素:" + newQueue.print());
        console.log("打印队列中移除元素:" + newQueue.dequeue());
        console.log("移除队列元素后再次打印队列中所有元素:" + newQueue.print());
        console.log("判断队列中是否有元素:" + newQueue.isEmpty());
        console.log("移除队列中所有元素:" + newQueue.clear());
        console.log("移除后判断队列中是否有元素:" + newQueue.isEmpty());

        newQueue.priorityEnqueue(['gannicus', 3]);
        newQueue.priorityEnqueue(['spartacus', 1]);
        newQueue.priorityEnqueue(['crixus', 2]);
        newQueue.priorityEnqueue(['oenomaus', 4]);

        console.log("优先队列中所有元素:" + newQueue.print());

    </script>

 Linked List(链表)

链表是一种链式数据结构,链上的每个节点包含两种信息:节点本身的数据和指向下一个节点的指针。链表和传统的数组都是线性的数据结构,存储的都是一个序列的数据,但也有很多区别,如下表:

比较维度数组链表
内存分配 静态内存分配,编译时分配且连续 动态内存分配,运行时分配且不连续
元素获取 通过Index获取,速度较快 通过遍历顺序访问,速度较慢
添加删除元素 因为内存位置连续且固定,速度较慢 因为内存分配灵活,只有一个开销步骤,速度更快
空间结构 可以是一维或者多维数组 可以是单向、双向或者循环链表

一个单向链表通常具有以下方法:

  1. size:返回链表中节点的个数
  2. head:返回链表中的头部元素
  3. add:向链表尾部增加一个节点
  4. remove:删除某个节点
  5. indexOf:返回某个节点的index
  6. elementAt:返回某个index处的节点
  7. addAt:在某个index处插入一个节点
  8. removeAt:删除某个index处的节点

单向链表的Javascript实现:

    <script type="text/javascript">
        /**
     * 链表中的节点 
     */
        function Node(element) {
            // 节点中的数据
            this.element = element;
            // 指向下一个节点的指针
            this.next = null;
        }

        function LinkedList() {
            var length = 0;
            var head = null;
            //返回链表中节点的个数
            this.size = function () {
                return length;
            }
            //返回链表中的头部元素
            this.head = function () {
                return head;
            }
            //向链表尾部增加一个节点
            this.add = function (element) {
                var node = new Node(element);
                if (head == null) {
                    head = node;
                } else {
                    var currentNode = head;

                    while (currentNode.next) {
                        currentNode = currentNode.next;
                    }

                    currentNode.next = node;
                }
                length++;
            }
            //删除某个节点
            this.remove = function (element) {
                var currentNode = head;
                var previousNode;
                if (currentNode.element === element) {
                    head = currentNode.next;
                } else {
                    while (currentNode.element !== element) {
                        previousNode = currentNode;
                        currentNode = currentNode.next;
                    }
                    previousNode.next = currentNode.next;
                }
                length--;
            }

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

            //返回链表所有节点
            this.value = function () {
                var currentNode = head;
                var nodeList = [];
                if (currentNode==null) {
                    return undefined
                } else {
                    while (currentNode.next) {
                        nodeList.push(currentNode.element);
                        currentNode = currentNode.next;
                    }
                    nodeList.push(currentNode.element);
                }
                return nodeList;
            }

            //返回某个节点的index
            this.indexOf = function (element) {
                var currentNode = head;
                var index = -1;
                while (currentNode) {
                    index++;
                    if (currentNode.element === element) {
                        return index;
                    }
                    currentNode = currentNode.next;
                }

                return -1;
            }
            //返回某个index处的节点
            this.elementAt = function (index) {
                var currentNode = head;
                var count = 0;
                while (count < index) {
                    count++;
                    currentNode = currentNode.next;
                }
                return currentNode.element;
            }
            //在某个index处插入一个节点
            this.addAt = function (index, element) {
                var node = new Node(element);
                var currentNode = head;
                var previousNode;
                var currentIndex = 0;

                if (index > length) {
                    return false;
                }

                if (index === 0) {
                    node.next = currentNode;
                    head = node;
                } else {
                    while (currentIndex < index) {
                        currentIndex++;
                        previousNode = currentNode;
                        currentNode = currentNode.next;
                    }
                    node.next = currentNode;
                    previousNode.next = node;
                }
                length++;
            }
            //删除某个index处的节点
            this.removeAt = function (index) {
                var currentNode = head;
                var previousNode;
                var currentIndex = 0;
                if (index < 0 || index >= length) {
                    return null;
                }
                if (index === 0) {
                    head = currentIndex.next;
                } else {
                    while (currentIndex < index) {
                        currentIndex++;
                        previousNode = currentNode;
                        currentNode = currentNode.next;
                    }
                    previousNode.next = currentNode.next;
                }
                length--;
                return currentNode.element;
            }
        }

        var newLinkedList = new LinkedList();
        newLinkedList.indexOf("第一个节点");
        newLinkedList.add("第一个节点");
        newLinkedList.add("第二个节点");
        newLinkedList.add("第三个节点");
        newLinkedList.add("第四个节点");
        newLinkedList.add("第五个节点");
        newLinkedList.add("第六个节点");
        newLinkedList.add("第七个节点");
        newLinkedList.add("第八个节点");
        console.log("打印链表中所有元素:" + newLinkedList.value());
        newLinkedList.remove("第一个节点");
        newLinkedList.remove("第三个节点");
        console.log("打印删除后链表中所有元素:" + newLinkedList.value());
    </script>

单链表

talk is easy, show the code, 下面用 JavaScript 实现一个相对完整的单链表,并提供以下方法供调用:

  1. push(element) // 链表尾部插入节点
  2. pop() // 链表尾部删除节点
  3. shift() // 删除头部节点、
  4. unshift(element) // 插入头部节点
  5. find(index) // 查找指定位置节点
  6. insert(element, index) // 指定位置插入节点
  7. edit(element, index) // 修改指定位置节点
  8. delete(index) // 链表删除指定位置节点
  9. cycle() // 使链表首尾成环
function initList() {
    class Node {
        constructor(item) {
            this.element = item
        }
    }
    class List {
        constructor() {
            this.head = null
            this.size = 0
            this.last = null
        }
        /**
        * 链表查找元素
        * @param index 查找的位置
        */
        find(index) {
            let current = this.head
            for (let i = 0; i < index; i++) {
                current = current.next
            }
            return current
        }
        /**
        * 链表尾部插入元素
        * @param element 插入的元素
        */
        push(element) {
            let newNode = new Node(element)
            if (this.size === 0) {
                this.head = newNode
                this.head.next = null
                this.last = this.head
            } else {
                this.last.next = newNode
                this.last = newNode
                newNode.next = null
            }
            this.size += 1
        }
        /**
        * 链表尾部删除元素
        * @param element 删除的位置
        */
        pop(element) {
            this.last.next = null
        }
        /**
        * 链表头部删除元素
        */
        shift() {
            if (this.size === 0)
                return
            this.head = this.head.next
            if (this.size === 1)
                this.last = null
            this.size -= 1
        }
        /**
        * 链表头部插入元素
        * @param element 插入的元素
        */
        unshift(element) {
            let newNode = new Node(element)
            newNode.next = this.head
            this.head = newNode
            if (this.size === 0)
                this.last = this.head
            this.size += 1
        }
        /**
        * 链表插入元素
        * @param element 插入的位置, index 插入的位置
        */
        insert(element, index) {
            if (index < 0 || index > this.size) {
                console.error('超出链表节点范围')
                return
            }
            let newNode = new Node(element)
            if (this.size === 0) {
                // 空链表
                newNode.next = null
                this.head = newNode
                this.last = newNode
            } else if (index === 0) {
                // 插入头部
                newNode.next = this.head
                this.head = newNode
            } else if (index == this.size) {
                //插入尾部
                newNode.next = null
                this.last.next = newNode
                this.last = newNode
            } else {
                // 中间插入
                let preNode = this.find(index - 1)
                newNode.next = preNode.next
                preNode.next = newNode
            }
            this.size += 1
        }
        /*
        *链表编辑元素
        * @param element 编辑的元素,index 元素位置
        */
        edit(element, index) {
            let current = this.find(index)
            current.element = element
        }
        /*
        *链表删除元素
        * @param index 删除元素位置
        */
        delete(index) {
            let current = this.find(index)
            if (index === 0) {
                // 删除头节点
                this.head = this.head.next
            } else if (index === ((this.size) - 1)) {
                // 删除尾节点
                let preNode = this.find(index - 1)
                preNode.next = null
            } else {
                // 删除中间节点
                let preNode = this.find(index - 1)
                let nextNode = preNode.next.next
                let removeNode = preNode.next
                preNode.next = nextNode
            }
            this.size -= 1
        }
        /*
        *链表使首尾成环
        */
        cycle() {
            this.last.next = this.head
        }
    }
    return new List()
}

let list = initList()

双向链表

双向链表的特点就是添加了指向上一个节点的指针(prev),比较单链表来说,稍微复杂一些,也更强大,这里把上面的单链表修改一下。

function initList() {
    class Node {
        constructor(item) {
            this.element = item
            this.next = null
            this.prev = null
        }
    }
    class List {
        constructor() {
            this.head = null
            this.size = 0
            this.last = null
        }
        /**
        * 链表查找元素
        * @param index 查找的位置
        */
        find(index) {
            let current = this.head
            for (let i = 0; i < index; i++) {
                current = current.next
            }
            return current
        }
        /**
        * 链表尾部插入元素
        * @param element 插入的元素
        */
        push(element) {
            let newNode = new Node(element)
            if (this.size === 0) {
                this.head = newNode
                this.head.next = null
                this.last = this.head
            } else {
                this.last.next = newNode
                newNode.next = null
                newNode.prev = this.last
                this.last = newNode
            }
            this.size += 1
        }
        /**
        * 链表尾部删除元素
        * @param element 删除的位置
        */
        pop() {
            if (this.size === 0)
                return
            if (this.size === 1) {
                this.head = null
                this.last = null
            } else {
                this.last.prev.next = null
                this.last = this.last.prev
            }
            this.size -= 1
        }
        /**
        * 链表头部删除元素
        */
        shift() {
            if (this.size === 0)
                return
            if (this.size === 1) {
                this.head = null
                this.last = null
            } else {
                this.head = this.head.next
                this.head.prev = null
            }
            this.size -= 1
        }
        /**
        * 链表头部插入元素
        * @param element 插入的元素
        */
        unshift(element) {
            let newNode = new Node(element)
            if (this.size === 0) {
                this.head = newNode
                this.head.next = null
                this.last = this.head
            } else {
                this.head.prev = newNode
                newNode.next = this.head
                this.head = newNode
            }
            this.size += 1
        }
        /**
        * 链表插入元素
        * @param element 插入的位置, index 插入的位置
        */
        insert(element, index) {
            if (index < 0 || index > this.size) {
                console.error('超出链表节点范围')
                return
            }
            let newNode = new Node(element)
            if (this.size === 0) {
                // 空链表
                this.head = newNode
                this.head.next = null
                this.last = this.head
            } else if (index === 0) {
                // 插入头部
                this.head.pre = newNode
                newNode.next = this.head
                this.head = newNode
            } else if (index == this.size - 1) {
                //插入尾部
                newNode.next = null
                newNode.prev = this.last
                this.last.next = newNode
                this.last = newNode
            } else {
                // 中间插入
                let prevNode = this.find(index - 1)
                newNode.next = prevNode.next
                prevNode.next = newNode
                newNode.prev = prevNode
                newNode.next.prev = newNode
            }
            this.size += 1
        }
        /*
        *链表编辑元素
        * @param element 编辑的元素,index 元素位置
        */
        edit(element, index) {
            let current = this.find(index)
            current.element = element
        }
        /*
        *链表删除元素
        * @param index 删除元素位置
        */
        delete(index) {
            let current = this.find(index)
            if (index === 0) {
                // 删除头节点
                this.head = this.head.next
                this.prev = null
            } else if (index === ((this.size) - 1)) {
                // 删除尾节点
                let preNode = this.find(index - 1)
                preNode.next = null
            } else {
                // 删除中间节点
                let preNode = this.find(index - 1)
                let nextNode = preNode.next.next
                let removeNode = preNode.next
                preNode.next = nextNode
                nextNode.prev = preNode
            }
            this.size -= 1
        }
        /*
        *链表使首尾成环
        */
        cycle() {
            this.last.next = this.head
            this.head.prev = this.last
        }
    }
    return new List()
}
let list = new initList()

循环链表

循环链表可以是单链表也可以是双向链表,它的特点是最后一个节点的 next 指针指向的是 head 节点
而不是 null,上面代码已经提供了 cycle 方法来实现。

判断链表有环

主要有这些方法:

  1. 遍历链表,使每一个节点与之前节点比较,若有重复则为有环链表

  2. 定义一个 Map 对象,遍历链表到每一个节点,若 Map 中没有次节点 ID,则将节点 ID 为 key, 存入 Map ,每个节点判断一次,如果某个节点的 ID存在,证明链表成环

  3. 双指针法,举个例子来说,两个人在操场跑步,速度不同时,总会在某些时刻相遇,就是因为跑到是圆的(环),利用这个原理,定义一个循环和两个指向头节点的指针,一个每次移动一个节点,一个移动两个节点,如果是成环的链表,某个时刻必然会遇到同一个节点。

链表在前端开发中的应用

    1. 链表的特性表明其擅长修改,不擅查找,所以对于需要大量修改的操作,可以考虑用链表实现,但是前端往往处理的数据量不会大,所以这种场景的实际意义不是很大,个人感觉在框架的底层优化上,使用较多,业务开发中,数组够用。

    2. 链表因为是随机存储的,所以比较省内存,但是对动态语言 JavaScript 解释器来说,有自动的垃圾回收机制来管理内存,所以链表的这个优势就不明显了。

    3. 链表特殊的结构,感觉适合做轮播图(双向循环链表)、双向导航列表等

 Set(集合)

集合是数学中的一个基本概念,表示具有某种特性的对象汇总成的集体。在ES6中也引入了集合类型Set,Set和Array有一定程度的相似,不同的是Set中不允许出现重复的元素而且是无序的。

一个典型的Set应该具有以下方法:

  1. values:返回集合中的所有元素
  2. size:返回集合中元素的个数
  3. has:判断集合中是否存在某个元素
  4. add:向集合中添加元素
  5. remove:从集合中移除某个元素
  6. union:返回两个集合的并集
  7. intersection:返回两个集合的交集
  8. difference:返回两个集合的差集
  9. subset:判断一个集合是否为另一个集合的子集

使用Javascript可以将Set进行如下实现,为了区别于ES6中的Set命名为MySet:

        function MySet() {
            var collection = [];
            this.has = function (element) {
                return (collection.indexOf(element) !== -1);
            }

            this.values = function () {
                return collection;
            }

            this.size = function () {
                return collection.length;
            }

            this.add = function (element) {
                if (!this.has(element)) {
                    collection.push(element);
                    return true;
                }
                return false;
            }

            this.remove = function (element) {
                if (this.has(element)) {
                    index = collection.indexOf(element);
                    collection.splice(index, 1);
                    return true;
                }
                return false;
            }

            this.union = function (otherSet) {
                var unionSet = new MySet();
                var firstSet = this.values();
                var secondSet = otherSet.values();
                firstSet.forEach(function (e) {
                    unionSet.add(e);
                });
                secondSet.forEach(function (e) {
                    unionSet.add(e);
                });
                return unionSet;
            }

            this.intersection = function (otherSet) {
                var intersectionSet = new MySet();
                var firstSet = this.values();
                firstSet.forEach(function (e) {
                    if (otherSet.has(e)) {
                        intersectionSet.add(e);
                    }
                });
                return intersectionSet;
            }

            this.difference = function (otherSet) {
                var differenceSet = new MySet();
                var firstSet = this.values();
                firstSet.forEach(function (e) {
                    if (!otherSet.has(e)) {
                        differenceSet.add(e);
                    }
                });
                return differenceSet;
            }

            this.subset = function (otherSet) {
                var firstSet = this.values();
                return firstSet.every(function (value) {
                    return otherSet.has(value);
                });
            }
        }

最后,推荐大家使用Fundebug,一款很好用的BUG监控工具~

 Hash Table(哈希表/散列表)

Hash Table是一种用于存储键值对(key value pair)的数据结构,因为Hash Table根据key查询value的速度很快,所以它常用于实现Map、Dictinary、Object等数据结构。如上图所示,Hash Table内部使用一个hash函数将传入的键转换成一串数字,而这串数字将作为键值对实际的key,通过这个key查询对应的value非常快,时间复杂度将达到O(1)。Hash函数要求相同输入对应的输出必须相等,而不同输入对应的输出必须不等,相当于对每对数据打上唯一的指纹。

一个Hash Table通常具有下列方法:

  1. put()增加元素
  2. remove()移除元素
  3. get()获取元素

简易版本的Hash Table的Javascript实现

class HashTable{
    constructor(){
        this.table = []         // 散列表
    }
    /* 散列函数1:冲突相对比较 */
    loseloseHashCode(key){      // 通过ASCII码转换生成key
        let hash = 0
        for (let i = 0; i< key.length; i++) {
           hash += key[i].charCodeAt()      // 计算key的ASCII码,当使用2个不同的键值,通过散列函数生成一致的散列键值,这样会发生哈希碰撞,导致数据覆盖
        }
        return hash%37         // loseloseHashCode方法计算关键码的公式,即 ASCII码取余37
    }
    /* 新增元素 */
    put(key,value){
        const position = this.loseloseHashCode(key)       // 元素在散列表里的地址
        this.table[position] = value
    }
    /* 移除元素 */
    remove(key){
        this.table[this.loseloseHashCode(key)] = undefined
    }
    /* 获取元素 */
    get(key){
        return this.table[this.loseloseHashCode(key)]
    }
}

 

function betterHash (str, arr) {
    var H = 37;
    var total = 0;
    for (var i = 0; i < str.length; i++) {
        total += H * total + str.chatCodeAt(i);
    }
    total = total % arr.length;
    return parseInt(total);
}
function get (key) {
    return this.table[this.betterHash(key, this.table)];
}

质数37是一个比较好的数字适合我们的哈希算法来参与哈希键值运算,并且使生成的键值在哈希表中均匀分布。

以对象实现哈希表

    // 创建构造函数HashTable
    function HashTable() {
        // 初始化哈希表的记录条数size
        var size = 0;

        // 创建对象用于接受键值对
        var res = {};

        // 添加关键字,无返回值
        this.add = function (key, value) {

            //判断哈希表中是否存在key,若不存在,则size加1,且赋值 
            if (!this.containKey(key)) {
                size++;
            }

            // 如果之前不存在,赋值; 如果之前存在,覆盖。
            res[key] = value;
        };

        // 删除关键字, 如果哈希表中包含key,并且delete返回true则删除,并使得size减1
        this.remove = function (key) {
            if (this.containKey(key) && (delete res[key])) {
                size--;
            }
        };

        // 哈希表中是否包含key,返回一个布尔值
        this.containKey = function (key) {
            return (key in res);
        };

        // 哈希表中是否包含value,返回一个布尔值
        this.containValue = function (value) {

            // 遍历对象中的属性值,判断是否和给定value相等
            for (var prop in res) {
                if (res[prop] === value) {
                    return true;
                }
            }
            return false;
        };

        // 根据键获取value,如果不存在就返回null
        this.getValue = function (key) {
            return this.containKey(key) ? res[key] : null;
        };

        // 获取哈希表中的所有value, 返回一个数组
        this.getAllValues = function () {
            var values = [];
            for (var prop in res) {
                values.push(res[prop]);
            }
            return values;
        };

        // 根据值获取哈希表中的key,如果不存在就返回null
        this.getKey = function (value) {
            for (var prop in res) {
                if (res[prop] === value) {
                    return prop;
                }
            }

            // 遍历结束没有return,就返回null
            return null;
        };

        // 获取哈希表中所有的key,返回一个数组
        this.getAllKeys = function () {
            var keys = [];
            for (var prop in res) {
                keys.push(prop);
            }
            return keys;
        };

        // 获取哈希表中记录的条数,返回一个数值
        this.getSize = function () {
            return size;
        };

        // 清空哈希表,无返回值
        this.clear = function () {
            size = 0;
            res = {};
        };
    }

 Tree(树)

Tree的数据结构和自然界中的树极其相似,有根、树枝、叶子,如上图所示。Tree是一种多层数据结构,与Array、Stack、Queue相比是一种非线性的数据结构,在进行插入和搜索操作时很高效。在描述一个Tree时经常会用到下列概念:

  1. Root(根):代表树的根节点,根节点没有父节点

  2. Parent Node(父节点):一个节点的直接上级节点,只有一个

  3. Child Node(子节点):一个节点的直接下级节点,可能有多个

  4. Siblings(兄弟节点):具有相同父节点的节点

  5. Leaf(叶节点):没有子节点的节点

  6. Edge(边):两个节点之间的连接线

  7. Path(路径):从源节点到目标节点的连续边

  8. Height of Node(节点的高度):表示节点与叶节点之间的最长路径上边的个数

  9. Height of Tree(树的高度):即根节点的高度

  10. Depth of Node(节点的深度):表示从根节点到该节点的边的个数

  11. Degree of Node(节点的度):表示子节点的个数

以二叉查找树为例,展示树在Javascript中的实现。在二叉查找树中,即每个节点最多只有两个子节点,而左侧子节点小于当前节点,而右侧子节点大于当前节点,如图所示:

二叉查找树应该具有以下常用方法:

  1. add:向树中插入一个节点
  2. findMin:查找树中最小的节点
  3. findMax:查找树中最大的节点
  4. find:查找树中的某个节点
  5. isPresent:判断某个节点在树中是否存在
  6. remove:移除树中的某个节点

以下是二叉查找树的Javascript实现:

        class Node {
            constructor(data, left = null, right = null) {
                this.data = data;
                this.left = left;
                this.right = right;
            }
        }

        class BST {
            constructor() {
                this.root = null;
            }

            add(data) {
                const node = this.root;
                if (node === null) {
                    this.root = new Node(data);
                    return;
                } else {
                    const searchTree = function (node) {
                        if (data < node.data) {
                            if (node.left === null) {
                                node.left = new Node(data);
                                return;
                            } else if (node.left !== null) {
                                return searchTree(node.left);
                            }
                        } else if (data > node.data) {
                            if (node.right === null) {
                                node.right = new Node(data);
                                return;
                            } else if (node.right !== null) {
                                return searchTree(node.right);
                            }
                        } else {
                            return null;
                        }
                    };
                    return searchTree(node);
                }
            }

            findMin() {
                let current = this.root;
                while (current.left !== null) {
                    current = current.left;
                }
                return current.data;
            }

            findMax() {
                let current = this.root;
                while (current.right !== null) {
                    current = current.right;
                }
                return current.data;
            }

            find(data) {
                let current = this.root;
                while (current.data !== data) {
                    if (data < current.data) {
                        current = current.left
                    } else {
                        current = current.right;
                    }
                    if (current === null) {
                        return null;
                    }
                }
                return current;
            }

            isPresent(data) {
                let current = this.root;
                while (current) {
                    if (data === current.data) {
                        return true;
                    }
                    if (data < current.data) {
                        current = current.left;
                    } else {
                        current = current.right;
                    }
                }
                return false;
            }

            remove(data) {
                const removeNode = function (node, data) {
                    if (node == null) {
                        return null;
                    }
                    if (data == node.data) {
                        // node没有子节点
                        if (node.left == null && node.right == null) {
                            return null;
                        }
                        // node没有左侧子节点
                        if (node.left == null) {
                            return node.right;
                        }
                        // node没有右侧子节点
                        if (node.right == null) {
                            return node.left;
                        }
                        // node有两个子节点
                        var tempNode = node.right;
                        while (tempNode.left !== null) {
                            tempNode = tempNode.left;
                        }
                        node.data = tempNode.data;
                        node.right = removeNode(node.right, tempNode.data);
                        return node;
                    } else if (data < node.data) {
                        node.left = removeNode(node.left, data);
                        return node;
                    } else {
                        node.right = removeNode(node.right, data);
                        return node;
                    }
                }
                this.root = removeNode(this.root, data);
            }
        }

测试一下:

const bst = new BST();

bst.add(4);
bst.add(2);
bst.add(6);
bst.add(1);
bst.add(3);
bst.add(5);
bst.add(7);
bst.remove(4);
console.log(bst.findMin());
console.log(bst.findMax());
bst.remove(7);
console.log(bst.findMax());
console.log(bst.isPresent(4));

打印结果:

1
7
6
false

二叉树的遍历,遍历方式:

   前序遍历:先遍历根结点,然后左子树,再右子树

   中序遍历:先遍历左子树,然后根结点,再右子树

   后续遍历:先遍历左子树,然后右子树,再根结点

//前序遍历
function ProOrderTraverse(biTree) {
    if (biTree == null) return;
    console.log(biTree.data);
    ProOrderTraverse(biTree.lChild);
    ProOrderTraverse(biTree.rChild);
}

//中序遍历
function InOrderTraverse(biTree) {
    if (biTree == null) return;
    InOrderTraverse(biTree.lChild);
    console.log(biTree.data);
    InOrderTraverse(biTree.rChild);
}

//后续遍历
function PostOrderTraverse(biTree) {
    if (biTree == null) return;
    PostOrderTraverse(biTree.lChild);
    PostOrderTraverse(biTree.rChild);
    console.log(biTree.data);
} 

二叉树的非递归遍历

  深度优先遍历(主要利用栈的先进后出)

  广度优先遍历(主要利用队列的先进先出)

//深度优先非递归
function DepthFirstSearch(biTree) {
    let stack = [];
    stack.push(biTree);

    while (stack.length != 0) {
        let node = stack.pop();
        console.log(node.data);
        if (node.rChild) {
            stack.push(node.rChild);
        }
        if (node.lChild) {
            stack.push(node.lChild);
        }

    }

}


//广度优先非递归
function BreadthFirstSearch(biTree) {
    let queue = [];
    queue.push(biTree);
    while (queue.length != 0) {
        let node = queue.shift();
        console.log(node.data);
        if (node.lChild) {
            queue.push(node.lChild);
        }
        if (node.rChild) {
            queue.push(node.rChild);
        }
    }

}

 Trie(字典树,读音同try)

Trie也可以叫做Prefix Tree(前缀树),也是一种搜索树。Trie分步骤存储数据,树中的每个节点代表一个步骤,trie常用于存储单词以便快速查找,比如实现单词的自动完成功能。 Trie中的每个节点都包含一个单词的字母,跟着树的分支可以可以拼写出一个完整的单词,每个节点还包含一个布尔值表示该节点是否是单词的最后一个字母。

Trie一般有以下方法:

  1. add:向字典树中增加一个单词
  2. isWord:判断字典树中是否包含某个单词
  3. print:返回字典树中的所有单词
        /**
         * Trie的节点
         */
        function Node() {
            this.keys = new Map();
            this.end = false;
            this.setEnd = function () {
                this.end = true;
            };
            this.isEnd = function () {
                return this.end;
            }
        }

        function Trie() {
            this.root = new Node();

            this.add = function (input, node = this.root) {
                if (input.length === 0) {
                    node.setEnd();
                    return;
                } else if (!node.keys.has(input[0])) {
                    node.keys.set(input[0], new Node());
                    return this.add(input.substr(1), node.keys.get(input[0]));
                } else {
                    return this.add(input.substr(1), node.keys.get(input[0]));
                }
            }

            this.isWord = function (word) {
                let node = this.root;
                while (word.length > 1) {
                    if (!node.keys.has(word[0])) {
                        return false;
                    } else {
                        node = node.keys.get(word[0]);
                        word = word.substr(1);
                    }
                }
                return (node.keys.has(word) && node.keys.get(word).isEnd()) ? true : false;
            }

            this.print = function () {
                let words = new Array();
                let search = function (node = this.root, string) {
                    if (node.keys.size != 0) {
                        for (let letter of node.keys.keys()) {
                            search(node.keys.get(letter), string.concat(letter));
                        }
                        if (node.isEnd()) {
                            words.push(string);
                        }
                    } else {
                        string.length > 0 ? words.push(string) : undefined;
                        return;
                    }
                };
                search(this.root, new String());
                return words.length > 0 ? words : null;
            }
        }

 Graph(图)

 

 

Graph是节点(或顶点)以及它们之间的连接(或边)的集合。Graph也可以称为Network(网络)。根据节点之间的连接是否有方向又可以分为Directed Graph(有向图)和Undrected Graph(无向图)。Graph在实际生活中有很多用途,比如:导航软件计算最佳路径,社交软件进行好友推荐等等。

Graph通常有两种表达方式:

Adjaceny List(邻接列表):

 

 

邻接列表可以表示为左侧是节点的列表,右侧列出它所连接的所有其他节点。

和 Adjacency Matrix(邻接矩阵):

邻接矩阵用矩阵来表示节点之间的连接关系,每行或者每列表示一个节点,行和列的交叉处的数字表示节点之间的关系:0表示没用连接,1表示有连接,大于1表示不同的权重。

访问Graph中的节点需要使用遍历算法,遍历算法又分为广度优先和深度优先,主要用于确定目标节点和根节点之间的距离,

图的相关术语

1.有一条边相连的顶点叫相邻顶点;
2.一个顶点的度就是该顶点的相邻顶点数;
3.路径指顶点组成的连续序列;
4.简单路径没有重复顶点;
5.有向图和无向图

图的表示
邻接矩阵

array[i][j] ===1代表i节点和j节点相邻,否则不相邻

邻接表

相当于把每个节点的相邻节点一一列举出来。

关联矩阵

形式和邻接矩阵一样,只是把邻接矩阵的直接维度换成对应的边,适用于边比顶点多的情况。

创建图类

接下来就采用邻接表的方式创建上面的图并且采用字典来表示:

创建字典类

        function Dictionary() {
            var items = {};

            //set(key,value)向字典里添加新元素,这里主要用来添加边
            this.set = function (key, value) {
                items[key] = value;
            }
            //has(key)如果存在就返回true,否则false
            this.has = function (key) {
                return key in items;
            }
            //get(key)通过key查找特定的数值并返回,这里主要用来查找顶点对应的边数组
            this.get = function (key) {
                return this.has(key) ? items[key] : undefined;
            }
        }

创建图类

        function Graph() {
            var vertices = [];    //用来储存顶点
            var adjList = new Dictionary();    //用来储存边

            //创建initializeColor用来初始化各个顶点的颜色,为遍历过程中的标记做准备
            var initializeColor = function () {
                var color = [];
                for (var i = 0; i < vertices.length; i++) {
                    color[vertices[i]] = 'white';
                }
                return color;
            }

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

            //addEdge(key,value)用来添加边v-w
            this.addEdge = function (v, w) {
                adjList.get(v).push(w);
                adjList.get(w).push(v);
            }

            //toString()把邻接表转化成字符串的形式,便于输出显示
            this.toString = function () {
                var s = '';
                for (var i = 0; i < vertices.length; i++) {
                    s += vertices[i] + '->';
                    var neighbors = adjList.get(vertices[i]);
                    for (var j = 0; j < neighbors.length; j++) {
                        s += neighbors[j] + ' ';
                    }
                    s += '\n';
                }
                return s;
            }
        }

创建实例

        var graph = new Graph();
        var myVertices = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
        //添加顶点
        for (var i = 0; i < myVertices.length; i++) {
            graph.addVertex(myVertices[i]);
        }

        //逐一加入边
        graph.addEdge('A', 'B');
        graph.addEdge('A', 'C');
        graph.addEdge('A', 'D');
        graph.addEdge('C', 'G');
        graph.addEdge('C', 'D');
        graph.addEdge('D', 'G');
        graph.addEdge('D', 'H');
        graph.addEdge('B', 'E');
        graph.addEdge('B', 'F');
        graph.addEdge('E', 'I');
        console.log(graph.toString());

输出的结果为:

A->B C D
B->A E F
C->A G D
D->A C G H
E->B I
F->B
G->C D
H->D
I->E

图的遍历
广度优先遍历

采用队列的方式,先添加节点的先被探索;
采用三种颜色来反应节点的状态:
白色:还没被访问;
灰色:被访问但未被探索;
黑色:被访问且探索过;

思路:
首先搜索节点A,探索A节点的相邻节点B,C,D,把其加入队列中,再逐一出队列进行探索,从而实现广度遍历。

添加bfs方法
//广度优先遍历,在Graph()类中添加以下方法

        this.bfs = function (v, callback) {
            var color = initializeColor();    //初始化节点,都标记为白色
            var queue = [];    //创建队列用来顶点的入队;
            queue.push(v);    //访问的节点入队列
            while (!queue.length == 0) {    //如果队列非空就执行以下
                var u = queue.shift();    //节点出队列
                var neighbors = adjList.get(u); //探索节点对应的边
                color[u] = 'grey';    //把搜索过的节点变成灰色
                for (var i = 0; i < neighbors.length; i++) {
                    var w = neighbors[i];
                    if (color[w] === 'white') {    //如果探索到的子节点是白色就逐一变灰并入队列
                        color[w] = 'grey';
                        queue.push(w);
                    }
                }
                color[u] = 'black';    //节点完成搜索和探索的过程,直接变黑
                if (callback) {
                    callback(u);    //回调函数,可以用来输出
                }
            }
        }

创建bfs实例

//bfs实例

        function printNode(value) {
            console.log('Visited vertex:' + value);
        }
        graph.bfs(myVertices[0], printNode);

bfs输出结果

Visited vertex:A
Visited vertex:B
Visited vertex:C
Visited vertex:D
Visited vertex:E
Visited vertex:F
Visited vertex:G
Visited vertex:H
Visited vertex:I

使用BFS寻找最短路径

this.BFS = function (v) {
            var color = initializeColor(),
                queue = [],
                d = [],    //用来储存从v到u的距离
                pred = [];    //用来储存节点的前溯点
            queue.push(v);

            for (var i = 0; i < vertices.length; i++) {
                d[vertices[i]] = 0;    //初始化
                pred[vertices[i]] = null;
            }

            while (!queue.length == 0) {
                var u = queue.shift();
                var neighbors = adjList.get(u);
                color[u] = 'grey';
                for (i = 0; i < neighbors.length; i++) {
                    var w = neighbors[i];
                    if (color[w] === 'white') {
                        color[w] = 'grey';
                        d[w] = d[u] + 1;    //从头节点到w的距离
                        pred[w] = u;
                        queue.push(w);
                    }
                }
                color[u] = 'black';
            }
            return {
                distance: d,
                predecessers: pred
            }
        }

创建BFS实例

        //BFS实例
        var shortestPathA = graph.BFS(myVertices[0]);//需要输入头节点myVertices[0]
        //console.log(shortestPathA);

        //搜索路径BFS
        var fromVertex = myVertices[0];
        for (var i = 1; i < myVertices.length; i++) {
            var toVertex = myVertices[i];
            var path = [];    //path用来储存路径
            for (var v = toVertex; v !== fromVertex; v = shortestPathA.predecessers[v]) {
                path.push(v);
            }
            path.push(fromVertex);
            var s = path.pop();
            while (!path.length == 0) {
                s += '-' + path.pop();
            }
            console.log(s)
        }

BFS输出结果

A-B
A-C
A-D
A-B-E
A-B-F
A-C-G
A-D-H
A-B-E-I

深度优先遍历

采用栈的方式,先添加节点的先被探索;
由递归实现。

思路:
从节点A开始,探索到A的相邻节点B,C,D压入栈中(这里的代码采用for循环,所以没有实质上的栈,但是用栈更容易理解),接着搜索B,探索到B的相邻节点E,F压入栈中,以此递归。

添加dfs方法

this.dfs = function (callback) {
            var color = initializeColor();
            for (var i = 0; i < vertices.length; i++) {
                if (color[vertices[i]] === 'white') {
                    dfsVisit(vertices[i], color, callback)//调用递归函数
                }
            }
        }

        var dfsVisit = function (u, color, callback) {
            color[u] = 'grey';
            if (callback) {
                callback(u);
            }
            var neighbors = adjList.get(u);
            for (var i = 0; i < neighbors.length; i++) {
                var w = neighbors[i];
                if (color[w] === 'white') {
                    dfsVisit(w, color, callback);
                }
            }
            color[u] = 'black';
        }

创建dfs实例

graph.dfs(printNode);

dfs输出结果
Visited vertex:A
Visited vertex:B
Visited vertex:E
Visited vertex:I
Visited vertex:F
Visited vertex:C
Visited vertex:G
Visited vertex:D
Visited vertex:H



posted @ 2019-10-06 17:05  圆圆测试日记  阅读(583)  评论(0编辑  收藏  举报