Javascript数据结构

加入QQ群:864680898,一起学习进步!点击群名可查看本人网站,有最新文章!

Javascript数据结构

数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成。记为:Data_Structure=(D,R),其中D是数据元素的集合,R是该集合中所有元素之间的关系的有限集合

数组

数组存储一系列同一种数据类型的值。但在JavaScript里,也可以在数组中保存不同类型的值。但我们还是要遵守最佳实践,别这么做(大多数语言都没这个能力)。

  • 数组的深复制

深复制的数组指向另一块内存空间

var arr = [1,2,3,4]
var arr1 = [].concat(arr)

arr1[0] = 'hello world'
console.log(arr, arr1)
  • 数组的淺复制

淺复制的数组还是指向原来的内存空间,也叫引用,改变arr1同样影响了arr

var arr = [1,2,3,4]
var arr1 = arr

arr1[0] = 'hello world'
console.log(arr, arr1)
  • 使用slice,concat深复制的局限性

当数组元素是对象时,整个拷贝还是浅拷贝,拷贝之后数组各个值的指针还是指向相同的存储地址。

var arr = [{name: 'mySkey'},2,3,4]
var arr1 = [].concat(arr)

arr1[0].name = 'hello world'
console.log(arr, arr1)

栈是遵从 先进后出,后进先出 的原则的有序集合。新添加的或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。例如:餐厅中重叠的盘子

  • 栈也被用在编程语言的编译器和内存中保存变量、方法调用等

在深入了解栈的应用前,我们先来学习如何使用Stack类,js中并没有,自己封装一个

var Stack = (function(){
  function Stack(){  }
  let _store = []
  Stack.prototype = {
    size(){
      return _store.length
    },
    isEmpty(){
      return _store.length === 0
    },
    pop(){
      return _store.pop()
    },
    push(element){
      _store.push(element)
    },
    peek(){
      if(!this.isEmpty()){
        return _store[_store.length-1]
      }
      return null
    },
    print(){
      return _store
    },
    clear(){
      _store = []
    }
  }
  return Stack
})()

var stack = new Stack()
stack.push(5)
stack.push(8)
stack.unshift(3)


console.log(stack.print())

使用es6的Symbol来封装Stack类,它是不可变的,可以用作对象的属性。

let _items = Symbol(); //{1}
class Stack {
  constructor () {
    this[_items] = []; //{2}
  }
  //Stack方法
}

或者使用WeakMap实现类,这种数据类型可以确保属性是私有的

const items = new WeakMap(); //{1}
class Stack {
  constructor () {
    items.set(this, []); //{2}
  }
  push(element) {
    let s = items.get(this); //{3}
    s.push(element);
  }
  pop() {
    let s = items.get(this);
    let r = s.pop();
    return r;
  }
  //其他方法
} 
  • 实际应用

栈的实际应用非常广泛。在回溯问题中,它可以存储访问过的任务或路径、撤销的操作

  • 数字的进制转换
function baseConverter(decNumber){
  var remStack = new Stack(),rem,binaryString = '', overZero = decNumber > 0 ? true : false;
  while (decNumber > 0){ 
    rem = Math.floor(decNumber % 2);
    remStack.push(rem);
    decNumber = Math.floor(decNumber / 2);
  }
  while (!remStack.isEmpty()){
    binaryString += remStack.pop().toString();
  }
  return binaryString;
}
console.log(baseConverter(5))

将上面代码调整一下,兼容十进制转化成8进制,16进制,在将十进制转成二进制时,余数是0或1;在将十进制转成八进制时,余数是0到7之间的数;但是将十进制转成16进制时,余数是0到9之间的数字加上A、B、C、D、E和F(对应10、11、12、13、14和15)

function baseConverter(decNumber, base){
  var remStack = new Stack(),rem,binaryString = '', overZero = decNumber > 0 ? true : false;
  while (decNumber > 0){ 
    rem = Math.floor(decNumber % base);
    if(base === 16){
      let numArr = new Array(10).fill(0).map((v,k)=>v=k).concat(['a','b','c','d','e','f'])
      rem = numArr[rem]
    }
    remStack.push(rem);
    decNumber = Math.floor(decNumber / base);
  }
  while (!remStack.isEmpty()){
    binaryString += remStack.pop().toString();
  }
  return binaryString;
}
console.log(baseConverter(255, 16))

队列

队列是遵循FIFO(First In First Out,先进先出,后进后出,也称为先来先服务)原则的一组有序的项。队列在尾部添加新元素,并从顶部移除元素。最新添加的元素必须排在队列的末尾。在现实中,最常见的队列的例子就是排队

在计算机科学中,常见的例子:

  • 1、打印队列:比如说我们需要打印五份文档。我们会打开每个文档,然后点击打印按钮。每个文档都会被发送至打印队列。第一个发送到打印队列的文档会首先被打印,以此类推,直到打印完所有文档
  • 2、消息队列:公众号推送、订阅推送、日志推送等场景都有应用
  • 3、弹幕:将每位用户发送的弹幕都缓存起来,按照先后时间顺序从屏幕中划过

使用javascript来实现一个Queue类

var Queue = (function(){
  function Queue(){  }
  let _store = []
  Queue.prototype = {
    size(){
      return _store.length
    },
    isEmpty(){
      return _store.length === 0
    },
    dequeue(){
      return _store.shift()
    },
    enqueue(element){
      _store.push(element)
    },
    front(){
      if(!this.isEmpty()){
        return _store[0]
      }
      return null
    },
    print(){
      console.log(_store)
    },
    clear(){
      _store = []
    }
  }
  return Queue
})()

let queue = new Queue(); 
queue.enqueue("John");
queue.enqueue("Jack"); 
queue.enqueue("rose"); 

queue.print()

queue.dequeue();
queue.print();
  • JavaScript 任务队列

当我们在浏览器中打开新标签时,就会创建一个任务队列。这是因为每个标签都是单线程处理所有的任务,它被称为事件循环。浏览器要负责多个任务,如渲染HTML,执行JavaScript代码,处理用户交互(用户输入、鼠标点击等),执行和处理异步请求。

链表

链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。就像是是寻宝游戏,你有一条线索,这条线索是指向寻找下一条线索的地点的指针。你顺着这条链接去下一个地点,得到另一条指向再下一处的线索。得到列表中间的线索的唯一办法,就是从起点(第一条线索)顺着列表寻找。

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

  • 双向链表

双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个节点的链接,而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素

  • 循环链表

循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用。循环链表和链表之间唯一的区别在于,最后一个元素指向下一个元素的指针(tail.next)不是引用null,而是指向第一个元素(head)

  • 双向循环链表

双向循环链表有指向head元素的tail.next,和指向tail元素的head.prev

集合

集合是由一组无序且唯一(即不能重复)的项组成的。这个数据结构使用了与有限集合相同的数学概念,但应用在计算机科学的数据结构中。

可以把集合想象成一个既没有重复元素,也没有顺序概念的数组。

  • 使用对象来实现集合Set(es6中已实现)
function Set() {
  let items = {};
  this.add = function(value){
    if (!this.has(value)){
      items[value] = value;
      return true;
    }
    return false;
  }
  this.remove = function(value){
    if (this.has(value)){
      delete items[value]
      return true;
    }
    return false;
  }
  this.has = function(value){
    return value in items;
  }
  this.clear = function(){
    items = {}
  }
  this.values = function(){
    let values = []
    for(let i in items){
      values.push(items[i])
    }
    return values
  }
  this.size = function(){
    return Object.keys(items).length
  }
}

let set = new Set()
set.add(15)
set.add(2)
console.log(set.values())
  • 并集:对于给定的两个集合,返回一个包含两个集合中所有元素的新集合
this.union = function(otherSet){
  let unionSet = new Set();
  let values = this.values();
  for (let i=0; i<values.length; i++){
    unionSet.add(values[i]);
  }
  values = otherSet.values();
  for (let i=0; i<values.length; i++){
    unionSet.add(values[i]);
  }
  return unionSet;
}
  • 交集:对于给定的两个集合,返回一个包含两个集合中共有元素的新集合
this.intersection = function(otherSet){
  let intersectionSet = new Set();
  let values = this.values();
  for (let i=0; i<values.length; i++){
    if (otherSet.has(values[i])){
      intersectionSet.add(values[i]);
    }
  }
  return intersectionSet;
}
  • 差集:对于给定的两个集合,返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合
this.difference = function(otherSet){
  let differenceSet = new Set();
  let values = this.values();
  for (let i=0; i<values.length; i++){
    if (!otherSet.has(values[i])){
      differenceSet.add(values[i]);
    }
  }
  return differenceSet;
}
  • 子集:验证一个给定集合是否是另一集合的子集
this.subset = function(otherSet){
  if (this.size() > otherSet.size()){
    return false;
  } else {
  let values = this.values();
  for (let i=0; i<values.length; i++){
    if (!otherSet.has(values[i])){
      return false;
    }
  }
  return true;
  }
}

字典

在字典中,存储的是[键,值]对,其中键名是用来查询特定元素的。字典和集合很相似,集合以[值,值]的形式存储元素,字典则是以[键,值]的形式来存储元素。字典也称作映射。ECMAScript 6同样包含了一个Map类的实现,即我们所说的字典。

  • 使用对象来实现Dictionary(es6中的Map)
function Dictionary() {
  var items = {};
  this.has = function(value){
    return value in items;
  }
  this.clear = function(){
    items = {}
  }
  this.values = function(){
    let values = []
    for(let i in items){
      if(this.has(i)){  
        /*我们不能仅仅使用for-in语句来遍历items对象的所有属性,还需要使用
        hasOwnProperty方法(验证items对象是否包含某个属性),因为对象的原
        型也会包含对象的其他属性(JavaScript基本的Object类中的属性将会被继
        承,并存在于当前对象中,而对于这个数据结构来说,我们并不需要它们)。*/
        values.push(items[i])
      }
    }
    return values
  }
  this.set = function(key, value) {
    items[key] = value;
  }
  this.delete= function(key) {
    if (this.has(key)) {
      delete items[key];
      return true;
    }
    return false;
  }
  this.get = function(key) {
    return this.has(key) ? items[key] : undefined;
  }
  this.keys = function() {
    return Object.keys(items);
  }
}

散列表

HashTable类,也叫HashMap类,它是Dictionary类的一种散列表实现方式。散列算法的作用是尽可能快地在数据结构中找到一个值。之前如果要在数据结构中获得一个值(使用get方法),需要遍历整个数据结构来找到它。如果使用散列函数,就知道值的具体位置,因此能够快速检索到该值。散列函数的作用是给定一个键值,然后返回值在表中的地址

  • 电子邮件地址簿

使用最常见的散列函数——“lose lose”散列函数,方法是简单地将每个键值中的每个字母的ASCII值相加

function HashTable() {
  var table = [];
  var loseloseHashCode = function(key){
    var hash = 0;
    for (var i = 0; i < key.length; i++) {
      hash += key.charCodeAt(i);
    }
    return hash % 37;
  }
  this.put = function(key, value){
    table[loseloseHashCode(key)] = value
  }
  this.get = function(key){
    return table[loseloseHashCode(key)]
  }
  this.remove = function(key){
    table[loseloseHashCode(key)] = undefined
  }
  this.print = function() {
    for (var i = 0; i < table.length; ++i) {
      if (table[i] !== undefined) {
        console.log(i + ": " + table[i]);
      }
    }
  }
}
var hash = new HashTable()
hash.put('name', 'mySkey')
hash.put('age', 23)

hash.print()

但是当key最终转成值相同时,就会导致数据丢失,这肯定是不可取的,比如

hash.put('name', 'mySkey')
hash.put('anme', 'hello world')
hash.print()

使用一个数据结构来保存数据的目的显然不是去丢失这些数据,而是通过某种方法将它们全部保存起来。因此,当这种情况发生的时候就要去解决它。处理冲突有几种方法:分离链接、线性探查和双散列法

  • 1、分离链接

分离链接法包括为散列表的每一个位置创建一个链表并将元素存储在里面。它是解决冲突的最简单的方法,但是它在HashTable实例之外还需要额外的存储空间。

  • 2、线性探查

另一种解决冲突的方法是线性探查。当想向表中某个位置加入一个新元素的时候,如果索引为index的位置已经被占据了,就尝试index+1的位置。如果index+1的位置也被占据了,就尝试index+2的位置,以此类推。

树是一种分层数据的抽象模型。现实生活中最常见的树的例子是家谱,或是公司的组织架构

一个树结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了顶部的第一个节点)以及零个或多个子节点:

位于树顶部的节点叫作根节点(11)。它没有父节点。树中的每个元素都叫作节点,节点分为内部节点和外部节点。至少有一个子节点的节点称为内部节点(7、5、9、15、13和20是内部节点)。没有子元素的节点称为外部节点或叶节点(3、6、8、10、12、14、18和25是叶节点)。

一个节点可以有祖先和后代。一个节点(除了根节点)的祖先包括父节点、祖父节点、曾祖父节点等。一个节点的后代包括子节点、孙子节点、曾孙节点等。例如,节点5的祖先有节点7和节点11,后代有节点3和节点6。

有关树的另一个术语是子树。子树由节点和它的后代构成。例如,节点13、12和14构成了上图中树的一棵子树。

节点的一个属性是深度,节点的深度取决于它的祖先节点的数量。比如,节点3有3个祖先节点(5、7和11),它的深度为3。

树的高度取决于所有节点深度的最大值。一棵树也可以被分解成层级。根节点在第0层,它的子节点在第1层,以此类推。上图中的树的高度为3(最大高度已在图中表示——第3层)。

  • 二叉树和二叉搜索树

二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。这些定义有助于我们写出更高效的向/从树中插入、查找和删除节点的算法。二叉树在计算机科学中的应用非常广泛。

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

  • 在js中实现BinarySearchTree
function BinarySearchTree() {
  var Node = function(key){
    this.key = key;
    this.left = null;
    this.right = null;
  };
  var root = null;
  var insertNode = function(node, newNode){
    if (newNode.key < node.key){
      if (node.left === null){
        node.left = newNode;
      } else {
        insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null){
        node.right = newNode;
      } else {
        insertNode(node.right, newNode); 
      }
    }
  }
  this.insert = function(key){
    var newNode = new Node(key);
    if (root === null){
      root = newNode;
    } else {
      insertNode(root,newNode);
    }
  }
}

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

遍历一棵树是指访问树的每个节点并对它们进行某种操作的过程。但是我们应该怎么去做呢?应该从树的顶端还是底端开始呢?从左开始还是从右开始呢?访问树的所有节点有三种方式:中序、先序和后序

  • 中序遍历

中序遍历是一种以上行顺序访问BST所有节点的遍历方式,也就是以从最小到最大的顺序访问所有节点。中序遍历的一种应用就是对树进行排序操作。

this.inOrderTraverse = function(callback){
  inOrderTraverseNode(root, callback);
}
var inOrderTraverseNode = function(node, callback){
  if(node !== null){
    inOrderTraverseNode(node.left, callback)  // 访问左侧的节点
    callback(node.key)
    inOrderTraverseNode(node.right, callback) // 访问右侧的节点
  }
}
  • 先序遍历

先序遍历是以优先于后代节点的顺序访问每个节点的。先序遍历的一种应用是打印一个结构化的文档

this.preOrderTraverse = function(callback){
  preOrderTraverseNode(root, callback);
}
var preOrderTraverseNode = function (node, callback) {
  if (node !== null) {
    callback(node.key);
    preOrderTraverseNode(node.left, callback);
    preOrderTraverseNode(node.right, callback);
  }
}
  • 后序遍历

后序遍历则是先访问节点的后代节点,再访问节点本身。后序遍历的一种应用是计算一个目录和它的子目录中所有文件所占空间的大小。

this.postOrderTraverse = function(callback){
  postOrderTraverseNode(root, callback);
}
var postOrderTraverseNode = function (node, callback) {
  if (node !== null) {
    postOrderTraverseNode(node.left, callback);  // {1}
    postOrderTraverseNode(node.right, callback); // {2}
    callback(node.key);                          // {3}
  }
}

中序、先序和后序遍历的实现方式是很相似的,唯一不同的是行{1}、{2}和{3}的执行顺序

  • 搜索最小值和最大值

最小值就是一直找左侧的子节点,最大值就是一直找右侧的子节点,知道最末端

this.min = function(){
  return minNode(root)
}
var minNode = function (node) {
  if (node){
    while (node && node.left !== null) {
      node = node.left;
    }
    return node.key;
  }
  return null;
}
this.max = function(){
  return maxNode(root)
}
var maxNode = function (node) {
  if (node){
    while (node && node.right !== null) {
      node = node.right;
    }
    return node.key;
  }
  return null;
}
  • 搜索一个特定的值

其实原理就是从根节点开始,比根节点小就找左边,反之找右边,递归下去

this.search = function(key){
  return searchNode(root, key);
};
var searchNode = function(node, key){
  if (node === null){
    return false;
  }
  if (key < node.key){
    return searchNode(node.left, key);
  } else if (key > node.key){
    return searchNode(node.right, key);
  } else {
    return true;
  }
}
  • 自平衡树

BST存在一个问题:取决于你添加的节点数,树的一条边可能会非常深;也就是说,树的一条分支会有很多层,而其他的分支却只有几层。这会在需要在某条边上添加、移除和搜索某个节点时引起一些性能问题。为了解决这个问题,有一种树叫作Adelson-Velskii-Landi树(AVL树)。AVL树是一种自平衡二叉搜索树,意思是任何一个节点左右两侧子树的高度之差最多为1。也就是说这种树会在添加或移除节点时尽量试着成为一棵完全树

  • Adelson-Velskii-Landi 树

AVL树是一种自平衡树。添加或移除节点时,AVL树会尝试自平衡。任意一个节点(不论深度)的左子树和右子树高度最多相差1。添加或移除节点时,AVL树会尽可能尝试转换为完全树。

1、计算平衡因子:

在AVL树中,需要对每个节点计算右子树高度(hr)和左子树高度(hl)的差值,该值(hr-hl)应为0、1或1。如果结果不是这三个值之一,则需要平衡该AVL树。这就是平衡因子的概念

2、AVL旋转:

右-右(RR):向左的单旋转

左-左(LL):向右的单旋转

左-右(LR):向右的双旋转

右-左(RL):向左的双旋转

尽管AVL树是自平衡的,其插入或移除节点的性能并不总是最好的。更好的选择是红黑树。红黑树可以高效有序地遍历其节点

堆是一种比较特殊的数据结构,可以被看做一棵树的数组对象,n个元素的序列{k1,k2,ki,…,kn}当且仅当满足下关系时,称之为堆。 (ki <= k2i,ki <= k2i+1)或者(ki >= k2i,ki >= k2i+1), (i = 1,2,3,4…n/2),满足前者的表达式的成为小顶堆,满足后者表达式的为大顶堆。具有以下的性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值
  • 堆总是一棵完全二叉树

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。

图是网络结构的抽象模型。图是一组由边连接的节点(或顶点)。

一个图G = (V, E)由以下元素组成。
V:一组顶点
E:一组边,连接V中的顶点

  • 有向图和无向图:

图可以是无向的(边没有方向)或是有向的(有向图)。如上图所示,有向图的边有一个方向

  • 强连通:

如果图中每两个顶点间在双向上都存在路径,则该图是强连通的。例如,C和D是强连通的,而A和B不是强连通的。

  • 加权的和未加权的

图还可以是未加权的或是加权的。加权图的边被赋予了权值

  • 图的表示

1、邻接矩阵

图最常见的实现是邻接矩阵。每个节点都和一个整数相关联,该整数将作为数组的索引。我们用一个二维数组来表示顶点之间的连接。如果索引为i的节点和索引为j的节点相邻,则array[i][j]=== 1,否则array[i][j] === 0,如下图所示:

不是强连通的图(稀疏图)如果用邻接矩阵来表示,则矩阵中将会有很多0,这意味着我们浪费了计算机存储空间来表示根本不存在的边。例如,找给定顶点的相邻顶点,即使该顶点只有一个相邻顶点,我们也不得不迭代一整行。邻接矩阵表示法不够好的另一个理由是,图中顶点的数量可能会改变,而2维数组不太灵活

2、邻接表

我们也可以使用一种叫作邻接表的动态数据结构来表示图。邻接表由图中每个顶点的相邻顶点列表所组成。存在好几种方式来表示这种数据结构。我们可以用列表(数组)、链表,甚至是散列表或是字典来表示相邻顶点列表。下面的示意图展示了邻接表数据结构

尽管邻接表可能对大多数问题来说都是更好的选择,但以上两种表示法都很有用,且它们有着不同的性质(例如,要找出顶点v和w是否相邻,使用邻接矩阵会比较快)。在本书的示例中,我们将会使用邻接表表示法。

3、关联矩阵

我们还可以用关联矩阵来表示图。在关联矩阵中,矩阵的行表示顶点,列表示边。如下图所示,我们使用二维数组来表示两者之间的连通性,如果顶点v是边e的入射点,则array[v][e] === 1;否则,array[v][e] === 0。

关联矩阵通常用于边的数量比顶点多的情况下,以节省空间和内存

参考文献

csdn数据结构文章

概述数据结构

《学习Javascript数据结构与算法》

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值