JavaScript 数据结构
一、概述
数据结构是指相互之间存在一种或多种特定关系的数据组成的集合。采用合适的数据结构能给提高开发或者存储效率。比如我们学习Js时接触过的数据结构(Set、Map),在合适的时机使用它们能帮助我们更快的解决问题。
本阶段我们会学习自己创建几种数据结构,来帮助我们在日后的开发中解决特定的某类问题。
二、栈
1. 创建栈结构
栈是一种遵循后进先出(LIFO)原则的有序数据集。
栈顶、栈底、入栈、出栈 栈结构也被用在编译器和内存中保存变量等
下面我们来创建栈数据结构。功能需求:
添加数据(push)
返回栈顶数据(peek)
从栈中删除栈顶数据并返回(pop)
清空栈(clear)
返回栈中数据个数(size)
class Stack{
constructor(){
this.items = [];
}
push(...rest){
rest.forEach(v=>{
this.items.push(v);
});
return this.items.length;
}
peek(){
return this.items[this.items.length-1];
}
pop(){
return this.items.pop();
}
clear(){
this.items = [];
}
size(){
return this.items.length;
}
}
检测是否满足栈的规则。
2. 栈结构的应用
- 进制转换 (Js自带进制转换api,此案例并不实用,仅作为理解原理)
//2进制转换
function baseConverter2(number){
let stack = new Stack();
let result = "";
while(number>0){
stack.push(number%2);
number = Math.floor(number/2);
}
while(stack.size()){
result += stack.pop();
}
return result||"0";
}
console.log(baseConverter2(8));
//16进制转换
function baseConverter16(number,base=2) {
let stack = new Stack();
let sign = "0123456789abcdefg";
let result = "";
while (number>0){
let remainder = number%base;
stack.push(sign[remainder]);
number = Math.floor(number/base);
}
while(stack.size()){
result += stack.pop();
}
return result||"0";
}
console.log(baseConverter16(15,16));
- 判断回文字符串
//判断回文
function isPlalindrome(word){
if( !word || typeof word !== "string" ){
throw "参数不为字符或字符为空,我们返回了false";
return false
};
let stack = new Stack();
let back = ""
stack.push(...word);
while (stack.size()){
back += stack.pop();
}
return back === word;
}
console.log(isPlalindrome("我爱你你爱我"));
console.log(isPlalindrome("123456"));
- 检测括号匹配
//检测括号匹配
function ifSignMatch(str){
let leftSign = "{[(",
rightSign = "}])";
let stack = new Stack();
for(let i=0,len=str.length;i<len;i++){
let char = str.charAt(i);
//遇到左括号,入栈
if( leftSign.indexOf(char) !== -1 ){
stack.push(char);
}
//遇到右括号,匹配
else if( rightSign.indexOf(char) !== -1 ){
if( leftSign.indexOf(stack.pop()) !== rightSign.indexOf(char) ){
return false;
}
}
}
return stack.size() === 0;
}
console.log(ifSignMatch("{[()]}"));
console.log(ifSignMatch("{[()]}}"));
3. 思考
this.items这可是一个任意都可以直接访问的数组,直接放着也沙雕了吧…想一下有什么样的方式可以避免外界的访问呢?比如 闭包?Symbol ?
三、队列
1. 创建队列结构
队列是一种遵循先进先出(FIFO)原则的有序数据集。
类似场景举例… 入队、出队
下面我们来创建队列数据结构。功能需求:
入队(enqueue)
出队(dequeue)
返回队列首端数据,但不删除(first)
清空队列(clear)
返回队列长度(size)
//队列结构
class Queue{
constructor(){
this.items = [];
}
enqueue(...rest){
rest.forEach(v=>{
this.items.push(v);
})
}
dequeue(){
return this.items.shift();
}
first(){
return this.items[0];
}
clear(){
this.items = [];
}
size(){
return this.items.length;
}
}
测试…
优化,避免直接访问items
let Queue = (function(){
let symbol = Symbol();
return class{
constructor(){
this[symbol] = [];
}
enqueue(...rest){
rest.forEach(v=>{
this[symbol].push(v);
});
}
dequeue(){
return this[symbol].shift();
}
first(){
return this[symbol][0];
}
clear(){
this[symbol] = [];
}
size(){
return this[symbol].length;
}
}
})();
2. 队列结构的应用
队列是极其基础的一种数据结构,不管在哪都能够运用到,通常都需要配合其他的代码实现强大的功能。比如整个JavaScript的执行机制,因为其实单线程,所以多任务之间都是队列的形式排好然后执行,再借助事件循环实现这个JavaScript强大的功能。比如jQuery中我们学习过的queue队列,最基本的结构就是我们刚刚介绍的队列。所以说我们其实在非常多的地方都在使用着队列,因为它实在太常见太实用了,只不过,使用时的api已经写好的而不需要我们自己来定义实现。
- 基于队列实现类似于jq的动画队列
//动画队列
let aq = (function(){
//可以将Queue提出,这样不仅仅在aq这个功能下能用到
class Queue{
constructor(){
this.items = [];
}
enqueue(...rest){
rest.forEach(v=>{
this.items.push(v);
});
}
dequeue(){
return this.items.shift();
}
first(){
return this.items[0];
}
clear(){
this.items = [];
}
size(){
return this.items.length;
}
}
//继承Queue,实现一个适合于当前需求的类
class _Queue extends Queue{
constructor(){
super();
this.ifDone = true;
}
run(){
if( !this.ifDone ){
return;
};
this.ifDone = false;
!function d(){
if( this.size() ){
new Promise(this.dequeue())
.then(d.bind(this));
}else{
this.ifDone = true;
}
}.call(this)
}
next(){
this.dequeue()();
}
}
//Map数据结构,用于
let animateQueue = new Map();
class Init{
constructor(selector){
this.dom = document.querySelector(selector);
}
animate(options,time=300){
if( !animateQueue.get(this.dom)){
animateQueue.set(this.dom,new _Queue());
}
let queue = animateQueue.get(this.dom);
queue.enqueue(function(res){
this.style.transition = time/1000+"s";
this.offsetWidth;
for (let [key,value] of Object.entries(options)) {
this.style[key] = value+'px';
}
setTimeout(res,time);
}.bind(this.dom));
queue.run();
return this;
}
}
return function(selector){
return new Init(selector)
}
})();
3. 优先队列
根据优先级来决定插入的顺序,就好像登机时的贵宾通道一样…
let PriorityQueue = (function(){
let symbol = Symbol();
class Priority{
constructor(ele,pri){
this.element = ele;
this.priority = pri;
}
}
return class{
constructor(){
this[symbol] = [];
}
enqueue(element,priority){
let pri = new Priority(element,priority);
let i=0;
for (;i<this.size();i++){
if( this[symbol][i].priority > priority ){
break;
}
}
this[symbol].splice(i,0,pri);
}
dequeue(){
return this[symbol].shift().element;
}
first(){
return this[symbol][0].element;
}
clear(){
this[symbol] = [];
}
size(){
return this[symbol].length;
}
}
})();
let priorityQueue = new PriorityQueue();
priorityQueue.enqueue("afei",1)
priorityQueue.enqueue("zhuque",0)
priorityQueue.enqueue("wula",1)
console.log(priorityQueue.dequeue());
console.log(priorityQueue.dequeue());
console.log(priorityQueue.dequeue());
此处我们是通过在入队的时候的排序实现优先级的,同样的,我们能不能从出队的角度来实现呢?或者,还有没有什么其他的方式来做呢?
4. 双向队列
允许从前面添加取出,也允许从后面添加取出。
//双向队列
let Queue = (function(){
let symbol = Symbol();
return class{
constructor(){
this[symbol] = [];
}
enqueueFront(ele){
this[symbol].unshift(ele);
}
enqueueBack(ele){
this[symbol].push(ele);
}
dequeueFront(){
return this[symbol].shift();
}
dequeueBack(){
return this[symbol].pop();
}
front(){
return this[symbol][0];
}
back(){
return this[symbol][this.size()-1];
}
size(){
return this[symbol].length;
}
}
})();
四、链表
1. 创建链表结构
存储多个数据常见的结构就是数组,它提供非常方便的[ ]操作符。但是缺点是,当我们往数组中插入或删除数据时,后面所有元素的位置都要进行一次变化。即使提供了便捷的splice函数,但内部原理上还是一样的。所以,当数据较多,并且会频繁的插入/删除数据时,再用数组来充当存储结构效率是较低的。
链表(Linked List)也可以用来存储有序的数据,它与数组的不同在于,并不是在内存中连续的存储,而是每一个节点由自身的数据和指向下一个节点的next组成,通过next指针将所有的节点串联起来形成类似于链的结构。
下面我们来创建链表结构:
链尾添加节点(append)
查找指定数据对应的节点(find)
指定数据后面插入节点(insert)
移除指定数据对应的节点(remove)
打印链表数据(print)
返回链表长度(size)
//可能额外需要的功能
查找数据对应索引(indexOf)
按照索引查找节点(find的参数设计)
按照索引插入节点(insert的参数设计)
按照索引移除节点(remove的参数数据)
//详细分解请看直播课讲解
let LinkedList = (function(){
let HEAD = Symbol();
class Node{
constructor(data){
this.data = data;
this.next = null;
}
}
return class{
constructor(){
this[HEAD] = null;
}
append(data){
let node = new Node(data);
let head = this[HEAD];
if( head === null ){
this[HEAD]=node;
}else{
while(head.next!==null){
head = head.next;
}
head.next = node;
}
}
find({data,index}){
let arr = [];
let head = this[HEAD];
if(head===null)return arr;
if( index === undefined ){
while(head!==null){
if( head.data === data ){
arr.push(head);
}
head = head.next;
}
}
if( typeof index === "number" ){
let i = 0;
while (head!==null){
if( i === index ){
arr.push(head);
}
head = head.next;
i ++;
}
}
return arr;
}
insert({data,newData,index}){
let head = this.find({data,index})[0];
if(!head)return false;
let node = new Node(newData);
let prevNext = head.next;
head.next = node;
node.next = prevNext;
return true;
}
remove({data,index}){
let head = this[HEAD];
if( index === undefined ){
if( head.data === data ){
this[HEAD] = head.next;
return;
}
while (head.next!==null){
if( head.next.data === data ){
head.next = head.next.next;
return;
}
head = head.next;
}
}
else if( typeof index === "number" ){
if( index === 0 ){
this[HEAD] = head.next;
return;
}
let i = 1;
while(head.next!==null){
if( i === index ){
head.next = head.next.next;
return;
}
head = head.next;
i++;
}
}
}
print(){
let head = this[HEAD];
while (head !== null){
console.log(head.data);
head = head.next;
}
}
size(){
let i = 0;
let head = this[HEAD];
while (head !== null){
i ++;
head = head.next;
}
return i;
}
}
})();
2. 双向链表与循环链表
双向链表指每个节点不仅仅有next指针,还有prev执行指向它的前一个节点。
循环链表指最后一个节点的next指针不是指向null,而是指向第一个节点。
代码思路与链表类似,可自己尝试实现。
3. 总结
与数组对比:①.取节点更麻烦,②.获取节点后做对应的添加和移除很方便,……所以当我们需要对数据进行多次的新增和移除操作时,选用链表结构。
五、集合
1. 创建集合结构
集合由一组无序且唯一的数据组成。是的没错,ES6的Set结构就是集合,无需我们自己再实现。接下来我们基于Set数据结构进行讨论。
回顾Set的apiadd delete has clear size
,以及初始传参。
2. 并集.交集.差集.子集
- 并集 — 传入两个集合,返回两个集合的合集
function union(a,b){
return new Set([...a,...b]);
}
- 交集 — 传入两个集合,返回两个集合共有的数据集合
function intersection(a,b){
let set = new Set();
for (let item of a) {
if( b.has(item) ){
set.add(item);
}
}
return set;
}
- 差集 — 传入两个集合,返回a集合中不存在于b集合的数据集合
function difference(a,b){
let set = new Set();
for (let item of a) {
if( !b.has(item) ){
set.add(item);
}
}
return set;
}
- 子集 — 传入两个集合,检测a集合是不是b集合的子集
function subset(a,b){
for (let item of a) {
if( !b.has(item) ){
return false;
}
}
return true;
}
六、字典
1. 创建字典结构
字典是一种以[键,值]的形式存储数据的数据结构,键用来查找查找对应的值。就像字典里面每个字(键)对应的有它的解释(值),再比如电话薄,名字(键)对应电话(值)。
Es6中的Map数据结构就是一种字典。
自己实现字典:
设置数据(set)
获取数据(get)
删除数据(delete)
是否存在(has)
返回长度(size)
let Map = (function(){
let symbol = Symbol();
return class{
constructor(){
this[symbol] = [];
}
set(key,value){
//遍历以检测是否有存在
for (let i = 0,len=this.size(); i < len; i++) {
let item = this[symbol][i];
if( item[0] === key ){
item[1] = value;
return;
}
}
this[symbol].push([key,value]);
}
get(key){
for (let i = 0,len=this.size(); i < len; i++) {
let item = this[symbol][i];
if( item[0] === key ){
return item[1];
}
}
return undefined;
}
delete(key){
for (let i = 0,len=this.size(); i < len; i++) {
let item = this[symbol][i];
if( item[0] === key ){
this[symbol].splice(i,1);
break;
}
}
}
has(key){
for (let i = 0,len=this.size(); i < len; i++) {
let item = this[symbol][i];
if( item[0] === key ){
return true;
}
}
return false;
}
size(){
return this[symbol].length;
}
}
})();
let map = new Map();
map.set("afei" , "15575127515");
map.set("zhuque" , "777");
map.set("xinai" , "55555");
map.set("afei" , "155");
console.log(map);
console.log(map.get("zhuque"));
console.log(map.has("xinai"));
console.log(map.has("ZHAOGE"));
map.delete("afei");
console.log(map.size());
七、哈希表
1. 创建哈希表结构
哈希表也叫散列表也是一种基于键值对存储值的一种数据结构,与之前的区别在于,哈希表能快速的定位值的位置,而不需要逐个遍历匹配。
JavaScript中的对象就具有哈希表结构的特性。
先忘记掉js的对象,我们通过数组来模拟一个哈希表。通过一系列的计算通过key值得到对应的序号,这个计算过程是建立哈希表最重要的过程,叫做哈希函数。比如我们可以通过每个字符的ANSII码相加得到对应的哈希索引:

let HashTable = (function(){
let symbol = Symbol();
let hashCode = function(key){
let hash = 0;
for(let i=0;i<key.length;i++){
hash += key.charCodeAt(i);
}
return hash;
};
return class{
constructor(){
this[symbol] = [];
}
set(key,value){
this[symbol][hashCode(key)] = value;
}
get(key){
return this[symbol][hashCode(key)];
}
}
})();
let h1 = new HashTable;
h1.set("afei","15575127515")
h1.set("zhuque","15714565897")
h1.set("xinai","15666666666")
console.log(h1);
很显然,这个哈希函数是很不合理的,第一,哈希值太大,只需要存三个数据却建立了一个长度好几百的数组,一般合理的利用率范围是 0.6-0.9,也就是说数据个数与总长度的比例在这之间。第二,容易出现重复,要是某两个字符码加起来刚好一样就会出现哈希值重复,从而覆盖前一个数据。
//解决第一个问题,我们可以预先设置一个较为合理的数组长度,然后规定序号不会超过长度:
//解决第二个问题,可以结合我们讲过的链表结构来存储,或者线性探查逐位往后推的方法:
//设定长度37,使用链表来解决冲突(使用质数来定长度是最合适的选择)
let LinkedList = (function(){
let HEAD = Symbol();
class Node{
constructor(data){
this.data = data;
this.next = null;
}
}
return class{
constructor(){
this[HEAD] = null;
}
append(data){
let node = new Node(data);
let head = this[HEAD];
if( head === null ){
this[HEAD]=node;
}else{
while(head.next!==null){
head = head.next;
}
head.next = node;
}
}
find({data,index}){
let arr = [];
let head = this[HEAD];
if(head===null)return arr;
if( index === undefined ){
while(head!==null){
if( head.data === data ){
arr.push(head);
}
head = head.next;
}
}
if( typeof index === "number" ){
let i = 0;
while (head!==null){
if( i === index ){
arr.push(head);
}
head = head.next;
i ++;
}
}
return arr;
}
insert({data,newData,index}){
let head = this.find({data,index})[0];
if(!head)return false;
let node = new Node(newData);
let prevNext = head.next;
head.next = node;
node.next = prevNext;
return true;
}
remove({data,index}){
let head = this[HEAD];
if( index === undefined ){
if( head.data === data ){
this[HEAD] = head.next;
return;
}
while (head.next!==null){
if( head.next.data === data ){
head.next = head.next.next;
return;
}
head = head.next;
}
}
else if( typeof index === "number" ){
if( index === 0 ){
this[HEAD] = head.next;
return;
}
let i = 1;
while(head.next!==null){
if( i === index ){
head.next = head.next.next;
return;
}
head = head.next;
i++;
}
}
}
print(){
let head = this[HEAD];
while (head !== null){
console.log(head.data);
head = head.next;
}
}
size(){
let i = 0;
let head = this[HEAD];
while (head !== null){
i ++;
head = head.next;
}
return i;
}
getHead(){
return this[HEAD];
}
}
})();
let HashTable = (function(){
let symbol = Symbol();
let hashCode = function(key){
let hash = 0;
for(let i=0;i<key.length;i++){
hash += key.charCodeAt(i);
}
return hash % 37;
};
let ValuePair = class{
constructor(key,value){
this.key = key;
this.value = value;
}
}
return class{
constructor(){
this[symbol] = [];
}
set(key,value){
let index = hashCode(key);
if( !this[symbol][index] ){
this[symbol][index] = new LinkedList();
}
this[symbol][index].append(new ValuePair(key,value));
}
get(key){
let index = hashCode(key);
let linked = this[symbol][index];
if( !linked )return undefined;
let head = linked.getHead();
while (head){
if( head.data.key === key ){
return head.data.value;
}
head = head.next;
}
return undefined;
}
}
})();
let h1 = new HashTable;
h1.set("afei","15575127515")
h1.set("zhuque","15714565897")
h1.set("xinai","15666666666")
h1.set("feia","15666666666")
console.log(h1);
console.log(h1.get("afei"));
console.log(h1.get("feia"));
//向下线性探索,解决冲突问题
let HashTable = (function(){
let symbol = Symbol();
let hashCode = function(key){
let hash = 0;
for(let i=0;i<key.length;i++){
hash += key.charCodeAt(i);
}
return hash % 37;
};
let ValuePair = class{
constructor(key,value){
this.key = key;
this.value = value;
}
}
return class{
constructor(){
this[symbol] = [];
}
set(key,value){
let index = hashCode(key);
while(this[symbol][index]){
if( this[symbol][index].key === key )break;
index ++;
index %= 37;
}
this[symbol][index] = new ValuePair(key,value);
}
get(key){
let index = hashCode(key);
let baseIndex = index;
if(!this[symbol][index])return undefined;
while (this[symbol][index]){
if( this[symbol][index].key === key ){
return this[symbol][index].value;
}
index ++;
index %= 37;
if( baseIndex === index )return undefined;
}
}
}
})();
let h1 = new HashTable;
h1.set("afei","15575127515")
h1.set("zhuque","15714565897")
h1.set("xinai","15666666666")
h1.set("feia","15666666666")
console.log(h1);
console.log(h1.get("afei"));
console.log(h1.get("feia"));
2. 有名的哈希函数
刚刚我们写的hash函数并不优秀,因为出现重复的概率比较高。下面列出一些程序猿前辈们总结的好用的hash函数,大家可以先感受一下
//DJB
function DJBHash(str) {
var hash = 5381;
var len = str.length , i = 0
while (len--){
hash = (hash << 5) + hash + str.charCodeAt(i++); /* times 33 */
}
hash &= ~(1 << 31); /* strip the highest bit */
return hash;
}
//JS
function JSHash(str) {
var hash = 1315423911;
for(var i = 0; i < str.length; i++) {
hash ^= ((hash << 5) + str.charCodeAt(i) + (hash >> 2));
}
return hash;
}
//PJW
function PJWHash( str) {
var BitsInUnsignedInt = 4 * 8;
var ThreeQuarters = (BitsInUnsignedInt * 3) / 4;
var OneEighth = (BitsInUnsignedInt / 8);
var HighBits = (0xFFFFFFFF) << (BitsInUnsignedInt - OneEighth);
var hash = 0;
var test = 0;
for(var i = 0; i < str.length; i++) {
hash = (hash << OneEighth) + str.charCodeAt(i);
if((test = hash & HighBits) != 0)
{
hash = (( hash ^ (test >> ThreeQuarters)) & (~HighBits));
}
}
return hash;
}
……
八、树
1. 了解树结构
树是一种重要的非线性结构,以分层的方式存储数据,比如公司的组织架构。
顶部节点叫做根节点,其他节点都是它的子节点,有子元素的节点称为内部节点,没有子元素的节点称为外部节点或叶节点。每个节点有层次,比如图中B节点层次为1,A节点层次为0,树种最大的层次称为该树的深度。每个节点和它的子节点组成子树。
2. 二叉树与二叉搜索树
二叉树在计算机科学中应用非常广泛。它是一种特殊的树,每个节点最多拥有左右两个子节点。利用它的特性我们可以更高效的对树中数据进行 增 删 查等操作。
二叉搜索树(BinarySearchTree)是二叉树的一种,它只允许在节点的左侧存储比节点小的值,在节点右侧存储比节点大(或等于)的值。
实现二叉搜索树:
插入节点 insert
let BST = (function(){
class Node{
constructor(data){
this.data = data;
this.left = null;
this.right = null;
}
}
let root = Symbol();
function insertNode(node,root) {
if (node.data < root.data) {
if (root.left) {
insertNode(node,root.left);
} else {
root.left = node;
}
}else{
if( root.right ){
insertNode(node,root.right);
}else{
root.right = node;
}
}
}
return class{
constructor(){
this[root] = null;
}
insert(data){
let node = new Node(data);
if (this[root] !== null) {
insertNode(node,this[root]);
} else {
this[root] = node;
}
}
}
})();
let bst = new BST();
bst.insert(9);
bst.insert(5);
bst.insert(4);
bst.insert(51);
bst.insert(88);
bst.insert(76);
bst.insert(23);
bst.insert(7);
bst.insert(1);
console.log(bst);
3. 树的遍历
讲课的时候分开讲每一种遍历的代码,课件里面代码我全放后面……
- 中序遍历针对每棵树都是左根右的顺序遍历数据:
- 先续遍历针对每棵树都是根左右的顺序遍历:团懒得画了
- 后续遍历针对每棵树都是左右根的顺序遍历:懒得画+1
let BST = (function(){
class Node{
constructor(data){
this.data = data;
this.left = null;
this.right = null;
}
}
let root = Symbol();
function insertNode(node,root) {
if (node.data < root.data) {
if (root.left) {
insertNode(node,root.left);
} else {
root.left = node;
}
}else{
if( root.right ){
insertNode(node,root.right);
}else{
root.right = node;
}
}
}
function inOrderTraverseNode(node,arr){
if( node === null )return;
inOrderTraverseNode(node.left,arr);
arr.push(node.data);
inOrderTraverseNode(node.right,arr);
}
function preOrderTraverseNode(node,arr){
if( node === null )return;
arr.push(node.data);
preOrderTraverseNode(node.left,arr);
preOrderTraverseNode(node.right,arr);
}
function postOrderTraverseNode(node,arr){
if( node === null )return;
postOrderTraverseNode(node.left,arr);
postOrderTraverseNode(node.right,arr);
arr.push(node.data);
}
return class{
constructor(){
this[root] = null;
}
insert(data){
let node = new Node(data);
if (this[root] !== null) {
insertNode(node,this[root]);
} else {
this[root] = node;
}
}
inOrderTraverse(){
let arr = [];
inOrderTraverseNode(this[root],arr);
return arr;
}
preOrderTraverse(){
let arr = [];
preOrderTraverseNode(this[root],arr);
return arr;
}
postOrderTraverse(){
let arr = [];
postOrderTraverseNode(this[root],arr);
return arr;
}
}
})();
let bst = new BST();
bst.insert(9);
bst.insert(5);
bst.insert(4);
bst.insert(51);
bst.insert(88);
bst.insert(76);
bst.insert(23);
bst.insert(7);
bst.insert(1);
console.log(bst);
console.log(bst.inOrderTraverse()); //在二叉搜索树种,这种方式正好可以得到顺序排列的数组
console.log(bst.preOrderTraverse());
console.log(bst.postOrderTraverse());
4. 查找与删除
//获取最小值 getMin
//获取最大值 getMax
//检测值是否存在 has
//移除值 remove
let BST = (function(){
let ROOT = Symbol();
class Node{
constructor(data){
this.data = data;
this.left = null;
this.right = null;
}
}
function insertNode(node,root) {
if (node.data < root.data) {
if (root.left) {
insertNode(node,root.left);
} else {
root.left = node;
}
}else{
if( root.right ){
insertNode(node,root.right);
}else{
root.right = node;
}
}
}
function inOrderTraverseNode(node,arr){
if( node === null )return;
inOrderTraverseNode(node.left,arr);
arr.push(node.data);
inOrderTraverseNode(node.right,arr);
}
function preOrderTraverseNode(node,arr){
if( node === null )return;
arr.push(node.data);
preOrderTraverseNode(node.left,arr);
preOrderTraverseNode(node.right,arr);
}
function postOrderTraverseNode(node,arr){
if( node === null )return;
postOrderTraverseNode(node.left,arr);
postOrderTraverseNode(node.right,arr);
arr.push(node.data);
}
function hasData(node,data) {
if( !node )return false;
if( data<node.data ){
return hasData(node.left,data);
}else if( data>node.data ){
return hasData(node.right,data);
}else{
return false;
}
}
function getMin(node){
while(node.left){
node = node.left;
}
return node;
}
function removeNode(node,data) {
if( !node )return node;
if( node.data < data ){
node.right = removeNode(node.right,data);
}else if( node.data > data ){
node.left = removeNode(node.left,data);
}else{
if( node.left === null && node.right === null ){
node = null;
return node;
}
if( node.left === null){
node = node.right;
return node;
}
if( node.right === null ){
node = node.left;
return node;
}
if( node.left && node.right ){
//方案1:左树最大值 或者 右树最小值 充当删除的这个值的位置
//方案2:把左树放置到右树的最左端
//方案3:把右树放置到左树的最右端
//方案1 ——
let min = getMin(node.right);
node.data = min.data;
node.right = removeNode(node.right,min.data);
return node;
}
}
}
return class{
constructor(){
this[ROOT] = null;
}
insert(data){
let node = new Node(data);
if (this[ROOT] !== null) {
insertNode(node,this[ROOT]);
} else {
this[ROOT] = node;
}
}
inOrderTraverse(){
let arr = [];
inOrderTraverseNode(this[ROOT],arr);
return arr;
}
preOrderTraverse(){
let arr = [];
preOrderTraverseNode(this[ROOT],arr);
return arr;
}
postOrderTraverse(){
let arr = [];
postOrderTraverseNode(this[ROOT],arr);
return arr;
}
getMin(){
let root = this[ROOT];
if(!root)return null;
while (root.left)root = root.left;
return root.data;
}
getMax(){
let root = this[ROOT];
if(!root)return null;
while (root.right)root = root.right;
return root.data;
}
has(data){
return hasData(this[ROOT],data);
}
remove(data){
this[ROOT] = removeNode(this[ROOT],data);
}
}
})();
5. 平衡二叉树
二叉搜索树有可能出现的一种情况是某一条分支深度特别大,而其他的分支深度很低。为了解决这个问题,有一种树叫做平衡二叉树(作者是 Adelson Velskii 和 Landi,所以也叫AVL树)。自平衡树在添加节点时,会按情况分配,保证树中任意节点的左子树和右子树的深度差不超过1。
创建平衡二叉树:
let AVL = (function(){
let ROOT = Symbol();
class Node{
constructor(key){
this.key = key;
this.left = null;
this.right = null;
}
}
//插入节点
function insertNode(root,node) {
if( !root )return node;
if( node.key < root.key ){
root.left = insertNode(root.left,node);
//检测树深度失衡
if( NodeDeep(root.left) - NodeDeep(root.right) > 1 ){
if( node.key < root.left.key ){
//左左
root = LL(root);
}else{
//左右
root = LR(root);
}
}
}else{
root.right = insertNode(root.right,node);
//检测树深度失衡
if( NodeDeep(root.right) - NodeDeep(root.left) > 1 ){
if( node.key < root.right.key ){
//右左
root = RL(root);
}else{
//右右
root = RR(root);
}
}
}
return root;
}
//计算树深度
function NodeDeep(node) {
if(!node)return 0;
return Math.max(NodeDeep(node.right),NodeDeep(node.left)) + 1;
}
//第一种情况,左左 LL
function LL(node) {
let tmp = node.left;
node.left = tmp.right;
tmp.right = node;
return tmp;
}
//第二种情况,右右 RR
function RR(node) {
let tmp = node.right;
node.right = tmp.left;
tmp.left = node;
return tmp;
}
//第三种情况,左右 LR
function LR(node) {
node.left = RR(node.left);
return LL(node);
}
//第四种情况,右左 RL
function RL(node) {
node.right = LL(node.right);
return RR(node);
}
return class {
constructor(){}
insert(key){
this[ROOT] = insertNode(this[ROOT],new Node(key));
}
}
})();
let avl = new AVL;
avl.insert(50)
avl.insert(30)
avl.insert(60)
avl.insert(58)
avl.insert(80)
avl.insert(59)
avl.insert(90)
avl.insert(100)
console.log(avl);
6. 红黑树
红黑树(Red Black Tree) 是平衡二叉树的一种。红黑树是一种特化的AVL树,在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的。 它可以在O(log n)时间内做查找,插入和删除,这里的 n 是树中元素的数目。
红黑树的特点:
- 节点必须是红色或黑色
- 根节点是黑色
- 所有叶子是都黑色(叶子是指null节点)
- 每个红色节点的子节点都是黑色(不能有连续的红节点)
- 任一节点到其每个叶子的所有路径都包含相同数量的黑色节点
红黑树例图:

创建红黑树:
// 红黑树模型
class RedBlackTree {
constructor() {
this.root = null;
}
// 插入节点
insert(key) {
let newNode = new Node(key);
let current = this.root;
let parent = null;
let color = RED;
// 如果根节点为null,则新节点为根节点
if (this.root === null) {
this.root = newNode;
newNode.color = BLACK;
return;
}
// 找到新节点的父节点
while (current !== null) {
parent = current;
if (newNode.key
九、图
1. 了解图结构
图由节点(顶点)和边组成,节点之间由边连接,一个边连接两个节点,一个节点可以对应很多条边(这些边的数量称为这个节点的度)。相邻节点按顺序组成的序列叫路径。路径中没有重复节点的叫做简单路径。路径中最后一个顶点和第一个顶点相同的构成环。有向图的边是带箭头的(有序的)。每个节点之间都有边的图叫强连通图。
2. 创建图
要存储图中的节点,并且表明节点间边的关系,我们可以采用多种方式来实现。常见的有邻接矩阵和邻接表。邻接矩阵使用二维数组来描述节点间连接关系,但是当节点比边多很多时,会有很多空位浪费。邻接表采用字典(也可以是表,数组)来存储节点和它对应连接的节点关系,具体模型图请看课程讲解。
使用邻接表的方案来实现图:
//图结构 类
let Graph = class {
constructor(){
//存储所有顶点
this.vertices = [];
//存储每个顶点对应的相邻顶点(边)
this.edges = {};
}
//添加顶点
addVertex(...rest){
rest.forEach(v=>{
//如果顶点已经添加,return
if( this.vertices.includes(v) )return;
//顶点不存在
//添加顶点
this.vertices.push(v);
//初始化顶点的边存储信息
this.edges[v] = [];
});
}
//添加边
addEdge(v1,v2){
//添加节点
this.addVertex(v1,v2);
//如果双方已经添加过对应的信息,返回
if( this.edges[v1].includes(v2) )return;
//双方互相添加边信息
this.edges[v1].push(v2);
this.edges[v2].push(v1);
}
};
let graph = new Graph;
graph.addEdge("A" , "B");
graph.addEdge("A" , "C");
graph.addEdge("A" , "E");
graph.addEdge("B" , "D");
graph.addEdge("B" , "E");
graph.addEdge("C" , "F");
graph.addEdge("E" , "F");
console.log(graph);
3. 遍历
图的遍历有两种算法:广度优先(Breadth-First Search)\和深度优先(Depth-First Search)。遍历图可以查找某个顶点、寻找两个顶点间的路径、检测图是否连通、检测图是否有环等。
遍历思路:将各个顶点分为三种状态-白、灰、黑,白色代表未被发现,灰色代表已被发现但未被探索,黑色代表已被发现并被探索。广度优先遍历过程是,先发现节点所有相邻节点,再遍历这些节点继续按规则往下遍历。深度优先遍历过程是,找到一个相邻节点后,立即再继续找它的相邻节点,以此类推。
//图结构 类
let Graph = (function(){
//队列类,辅助广度优先遍历
class Queue{
constructor(){
this.items = [];
}
enqueue(data){
this.items.push(data);
}
dequeue(){
return this.items.shift();
}
size(){
return this.items.length;
}
}
//图类
return class {
constructor(){
//存储所有顶点
this.vertices = [];
//存储每个顶点对应的相邻顶点(边)
this.edges = {};
}
//添加顶点
addVertex(...rest){
rest.forEach(v=>{
//如果顶点已经添加,return
if( this.vertices.includes(v) )return;
//顶点不存在
//添加顶点
this.vertices.push(v);
//初始化顶点的边存储信息
this.edges[v] = [];
});
}
//添加边
addEdge(v1,v2){
//添加节点
this.addVertex(v1,v2);
//如果双方已经添加过对应的信息,返回
if( this.edges[v1].includes(v2) )return;
//双方互相添加边信息
this.edges[v1].push(v2);
this.edges[v2].push(v1);
}
//广度优先
bfs(v){
let vertices = this.vertices,
edges = this.edges,
color = {},
queue = new Queue()
;
//对每个顶点添加状态标志
vertices.forEach(v=>{
color[v] = "white";
});
//入口顶点入队及状态改变
queue.enqueue(v);
color[v] = "grey";
//循环出队进行发现或探索
while (queue.size()){
//取出队首顶点
let u = queue.dequeue();
//获取顶点对应的相邻顶点
let edg = edges[u];
//测试打印
let s = u+"===>"
//探索
edg.forEach(v=>{
if(color[v] !== "white")return;
s += v+" "
queue.enqueue(v);
color[v] = "grey";
});
//探索完,改变状态
color[u] = "black";
console.log(s);
}
}
};
})();
let graph = new Graph;
graph.addEdge("A" , "B");
graph.addEdge("A" , "C");
graph.addEdge("A" , "E");
graph.addEdge("B" , "D");
graph.addEdge("B" , "E");
graph.addEdge("C" , "F");
graph.addEdge("E" , "F");
graph.bfs("A");
//图结构 类
let Graph = (function(){
//队列类,辅助广度优先遍历
class Queue{
constructor(){
this.items = [];
}
enqueue(data){
this.items.push(data);
}
dequeue(){
return this.items.shift();
}
size(){
return this.items.length;
}
}
//图类
return class {
constructor(){
//存储所有顶点
this.vertices = [];
//存储每个顶点对应的相邻顶点(边)
this.edges = {};
}
//添加顶点
addVertex(...rest){
rest.forEach(v=>{
//如果顶点已经添加,return
if( this.vertices.includes(v) )return;
//顶点不存在
//添加顶点
this.vertices.push(v);
//初始化顶点的边存储信息
this.edges[v] = [];
});
}
//添加边
addEdge(v1,v2){
//添加节点
this.addVertex(v1,v2);
//如果双方已经添加过对应的信息,返回
if( this.edges[v1].includes(v2) )return;
//双方互相添加边信息
this.edges[v1].push(v2);
this.edges[v2].push(v1);
}
//最短路径
BFS(v){
let vertices = this.vertices,
edges = this.edges,
color = [],
queue = new Queue(),
info = {}
;
//状态初始化
vertices.forEach(v=>{
color[v] = "white";
});
//入口顶点入队 状态改变 初始信息
queue.enqueue(v);
color[v] = "grey";
info[v] = {distance : 0, path : v};
//发现与探索
while (queue.size()){
let u = queue.dequeue();
edges[u].forEach(v=>{
if(color[v]!=="white")return;
color[v] = "grey";
queue.enqueue(v);
info[v] = {
distance : info[u].distance + 1,
path : info[u].path + "->" + v
};
});
color[u] = "black";
}
/*
返回信息的期望状态
{
"A" : {distance:0,path:"A"}
"B" : {distance:1,path:"A-B"}
"C" : {distance:1,path:"A-C"}
"D" : {distance:2,path:"A-B-D"}
"E" : {distance:1,path:"A-E"}
"F" : {distance:2,path:"A-C-F"}
}
*/
return info;
}
};
})();
let graph = new Graph;
graph.addEdge("A" , "B");
graph.addEdge("A" , "C");
graph.addEdge("A" , "E");
graph.addEdge("B" , "D");
graph.addEdge("B" , "E");
graph.addEdge("C" , "F");
graph.addEdge("E" , "F");
console.log(graph.BFS("D"));
评论(0)
暂无评论