使用JavaScript实现数据结构-数组,链表,树...

269 阅读13分钟

前言

​ 主要是数据结构与算法JavaScript描述 (Michael McMillan )的一些笔记整理。

​ 任何被递归定义的函数,都可以改写为迭代式的程序

​ js中数据结构都被实现为对象

1.数组

​ 一般为内建类型的数据结构,通过索引来任意存取,索引通常为数字,用来计算元素之间存储位置的偏移量。

​ JS中的数组是一种特殊的对象,索引是对象的属性,在内部转换为字符串类型

1.1 构造数组

let arr = new Array();//传参,1个参数:指定数组的长度,多个参数:数组的值
let arr = [];
arr.length = 0;//可用来置空,超出长度设置的数组项会销毁
arr[arr.length] //undefined,未赋值的数组项
str.split('')//字符串的split方法也可以生成数组
//构建二维数组,2行3列为0的二维数组
let Tarr = [...Array(2)].map(() => Array(3).fill(0))

1.2 操作数组

数组的赋值,可以通过循环赋值,索引赋值,数组之间的赋值需要用到深拷贝。

浅拷贝在拷贝对象时,对于基本数据类型的变量会重新复制一份,对引用类型的变量只是对引用进行拷贝,深拷贝会拷贝对象同时对引用指向的对象进行拷贝。

//数组赋值
let arr =[1,2,3];
let copyArr = []
for(let i=0;i<arr.length;i++){
    copyArr[i] = arr[i]
}
//或者通过数组方法进行深拷贝
copyArr = arr.concat() //concat方法会创建副本,返回新数组
copyArr = arr.slice(0) //slice方法会返回截取项组成的新数组

1.2.1 位置检测方法

indexOf():存在返回数组中的索引,不包含返回-1,多个元素返回第一个相同元素的索引

lastIndexOf():从后往前查找

1.2.2 数组转换为字符串

join():会返回所有元素用逗号隔开的字符串,可传参数,定义隔开字符

toString() 方法会返回所有元素用逗号隔开的字符串

1.2.3 数组排序方法

reverse():直接翻转数组项的顺序

sort():对数组进行排序,默认升序排列,比较的是字符串大小

arr.sort((a,b)=>{return a-b})//a-b:大到小;b-a:小到大

1.2.4 数组操作方法

push():添加数据到数组尾部,返回数组长度

pop():从数组尾部移除最后一项,返回移除的值

shift():从数组头部移除第一项,返回移除的值

unshift():在数组头部添加任意个项,返回修改后数组长度

1.2.5 迭代器

every():运行函数对每一项运行结果都返回true,则返回true。返回布尔值

filter():返回运行函数返回true的项组成的数组。返回满足条件的数组(原数组)

forEach():对每一项运行给定函数。无返回值

map():对每一项运行给定函数,返回函数调用后的结果组成的数组。返回结果数组

some():运行函数任一项返回true,则返回true。返回布尔值

reduce():接收两个参数,调用函数function,初始值;迭代所有的项,返回最终结果。调用函数接收四个参数,前一个值prev,当前值cur,项的索引index,数组对象array reduceRight():同上,迭代顺序不同,从后往前迭代

1.2.6 数组检测方法

let arr = [1,2,3];
Array.isArray(arr) //true
arr.instanceOf Array //true
arr.constructor === Array //true
//typeof 只能区分5种原始类型(number/string/boolean/null/undefined)和函数function,其他都区分为object

2.列表

2.1 定义和构造

列表是有序数据,根据列表的定义实现功能

function list(){
    this.listSize = 0; //列表元素个数
    this.pos = 0;      //当前位置
    this.dataStore = [];//空数组,用于列表元素存储
    this.clear=clear;   //清空列表中所有元素
    this.find = find;	//找到元素在列表中位置
    this.toString = toString;//字符串输出
    this.insert = insert;	//现有元素后插入新元素
    this.append = append;	//列表末尾添加新元素
    this.remove = remove;	//从列表中删除元素
    this.front = front;		//将列表的当前位置移动到第一个元素
    this.end = end;		//将列表的当前位置移动到最后一个元素
    this.prev = prev;		//将当前位置后移一位
    this.next = next;		//当当前位置前移一位
    this.length = length;	//列表中元素的个数
    this.currpos = currpos;     //返回列表的当前位置
    this.moveTo = moveTo;	//将当前位置移动到指定位置
    this.getElement = getElement;//返回当前位置的元素
    this.contains = contains;	
}
function clear(){
    delete this.dataStore;
    this.dataStore = [];
    this.listSize = this.pos = 0;
}
function find(ele){
    return this.dataStore.indexOf(ele)
}
function toString(){
    return this.dataStore.toString()
}
function insert(ele,after){
    var insertPos = this.find(after);
    if(insertPos > -1){
        this.dataStore.splice(insertPos+1,0,ele);
        ++this.listSize;
        return true
    }
    return false
}
function append(ele){
    this.dataStore.push(ele);
    this.listSize++
}
function remove(ele){
    var findAt = this.find(ele);
    if(findAt > -1){
        this.dataStore.splice(findAt,1);
        --this.listSize;
        return true
    }
    return false
}
function front(){
    this.pos = 0 ;
}
function end(){
    this.pos = this.listSize-1;
}
function prev(){
    if(this.pos >0){
        --this.pos;
    }
}
function next(){
    if(this.pos < this.listSzie-1){
        ++this.pos;
    }
}
function currPos(){
    return this.pos;
}
function moveTo(position){
    this.pos = position;
}
function getElement(){
    return this.dataStore[this.pos];
}

3.栈

栈是一种特殊的列表,元素只能通过列表的一端访问,即栈顶,所以称为后进先出(LIFO)的数据结构。

3.1 构造

入栈push(),出栈pop()

function  Stack(){
    this.dataStore = [];
    this.top = 0;//栈顶元素位置
    this.push = push;//入栈
    this.pop = pop;//出栈
    this.peek = peek;//获取栈顶元素
    this.clear = clear;//清空栈
    this.length = length;//栈长度
}
function push(ele){
    this.dataStore.push(ele)
    this.top ++
}
function opo(){
    return this.dataStore.pop()
    this.top --
}
function peek(){
    return this.dataStore[this.top-1]
}
function length(){
    return this.top
}
function clear(){
    this.top = 0;
}

3.2 应用

进制转换,回文字符,阶乘

//进制转换,2-9,数字n转换为以b为基的数字
function mulBase(num,base){
    let st = new Stack();
    do{
        st.push(num%base);
        num = Math.floor(num/=base);
    }while(num>0){
        let cov = '';
        while(s.lenght()>0){
            cov += s.pop();
        }
        return cov
    }
}
//回文数
function isPalindrome(str){
    let st = new Stack();
    for(let i=0;i<word.length;i++){
        st.push(str[i])
	}
    let rstr = '';
    while(st.length()>0){
        rstr += s.pop();
    }
    if(rstr == str){
        return true
    }
    return false    
}
//阶乘
function factorial(n){
    if(n==0) return 1;
    return n*factorial(n-1)
}
function fact(n){
    let st = new Stack();
    while(n>1){
        st.push(n--)
    }
    let res = 1;
    while(st.length()>0){
        res *= st.pop();
    }
    return res
}

4 队列

先进先出(FIFO)

4.1 构造

function Queue(){
    this.dataStore = [];
    this.enqueue = enqueue;//队列入
    this.dequeue = dequeue;//队列出
    this.front = front;
    this.back = back;
    this.toString = toString;
    this.empty = empty;
}
function enqueue(element) {
    this.dataStore.push(element);
} 
function dequeue() {
    return this.dataStore.shift();
} 
function front() {
    return this.dataStore[0];
} 
function back() {
    return this.dataStore[this.dataStore.length-1];
} 
function toString() {
    var retStr = "";
    for (var i = 0; i < this.dataStore.length; ++i) {
        retStr += this.dataStore[i] + "\n";
    }
    return retStr;
}
function empty() {
    return this.dataStore.length !== 0
}

5 链表

js的数组被实现为对象,跟其他语言的数组相比,效率很低。

链表是由一组节点组成的集合,每个节点都使用一个对象的引用指向后继,指向另一个节点的引用叫做链。链表的尾元素指向null节点。

5.1 构造

链表包含两个类,Node类用来表示节点,LinkedList类提供插入节点、删除节点、显示列表元素的方法等。

//element保存及诶单上数据,next指向下一个节点
function Node(element){
    this.element = element;
    this.next = null;
}
function LList(){
    this.head = new Node("head");
    this.find = find;//找到特定数据的节点
    this.insert = insert;//插入新节点
    this.remove = remove;//删除节点
    this.display = display;
}
//head节点的next属性被初始化为null,新元素插入时,next会指向新的元素
function find(item){//找到元素当前节点
    let currNode = this.head;
    while(curNode.element != item){
        currNode = currNode.next;
    }
    return currNode
}
function insert(newElement,item){//插入
    let newNode = new Node(newElement);
    let current = this.find(item);
    newNode.next = current.next;
    current.next = newNode;
}
function display(){//打印整个链表
    let currNode = this.head;
    while(!(currNode.next == null)){
        console.log(currNode.next.element);
        currNode = currNode.next;
    }
}
function findPrevious(item){//找到当前元素上一个节点
   let currentNode = this.head;
    while(!(currNode.next == null) && currNode.next.element != item){
        currNode = currNode.next
    }
    return currNode
}
function remove(item){
    let preNode = this.findPrevious(item);
    if(!(preNode.next == null)){
        preNode.next = preNode.next.next;
    }
}

5.2 双向链表

Node类新增属性,该属性指向前节点的链接,插入链表需要指出节点的前驱和后继,删除节点查找到当前节点就可以删除了。

//双向链表
function Node(element){
    this.element = element;
    this.next = null;
    this.previous = null;
}
//插入链表
function insert(NewElement,item){
    let newNode = new Node(newElement);
    let current = this.find(item);
    newNode.next = current.next;
    newNode.previous = current;
    current.next = newNode;
}
//删除链表
function remove(item){
    let currNode = this.find(item);
    if(!(currNode.next == null)){
        currNode.previous.next = currNode.next;
        currNode.next.previous = currNode.previous;
    }
    currNode.next = null;
    currNode.previous = null
}

5.3 循环链表

循环链表的头结点的next属性指向它本身,使整个链表的尾节点指向头节点。

function LList(){
    this.head = new Node("head");
    this.head.next = this.head;
    this.find = find;
    this.insert = insert;
    this.display = display;
    this.findPrevious = findPrevious;
    this.remove = remove;
}

6 字典

字典以键-值对形式存储的数据结构。

6.1 构造

基于数组的字典实现

function dictionary(){
    this.datastore = new Array()
    this.add = add
    this.find = find
    this.remove = remove
    this.showAll = showAll
    this.count = count
    this.clear = clear
}
function add(key,value){
    this.datastore[key] = value
}
function find(key){
    return this.datastore[key]
}
function remove(key){
    delete this.datastore[key]
}
function showAll(){
    for(var key in Object.keys(this.datastore)) {
        console.log(key + "->" + this.datastore[key])
    }
}
function  count(){
    var n = 0 ;
    for(var key in Object.keys(this.datastore)){
        ++n
    }
    return n
}
function clear(){
    for(var key in Object.keys(this.datastore)){
      delete this.datastore[key]
   }
}
function showAll(){
    for(var key in Object.keys(this.datastore).sort()) {
     console.log(key + "->" + this.datastore[key])
   }
}

tips:关于对象的一些方法

// Object.keys()
//ES5 引入了Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历( enumerable )属性的键名。
var obj = { foo: "bar", baz: 42 };
Object.keys(obj)
// ["foo", "baz"]

//Object.values()
//Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历( enumerable )属性的键值。
var obj = { foo: "bar", baz: 42 };
Object.values(obj)
// ["bar", 42]

var obj = { 100: 'a', 2: 'b', 7: 'c' };
Object.values(obj)
// ["b", "c", "a"]
//照数值大小,从小到大遍历的,因此返回的顺序是b、c、a

7.散列

散列是常用的数据存储技术,散列数据可以快速的插入或取用。

在散列中,两个键映射成同一个值的现象称为碰撞

7.1 构造

基于数组构造,数组的长度是预先设定的

function HashTable(){
    this.table = new Arrary(137);
    this.simpleHash = simpleHash;
    this.showDistro = showDistro;
    this.put = put
}
//字符串类型的键转换散列值
function simpleHash(data){
    let total = 0;
    for(let i=0;i<data.length;i++){
        total += data.charCodeAt(i);
    }
    return total % this.table.length;   
}
function put(data){
    var pos = this.simpleHash(data);
    this.table[pos] = data
}
function showDistro(){
    var n=0;
    for(var i=0;i<this.table.length;i++){
        if(this.table[i] != undefined){
            console.log(i+": "+this.table[i])
        }
    }
}
//字符串类型另一种计算散列值
function betterHash(string,arr){
    const H = 37;
    var total = 0;
    for(var i=0;i<string.length;i++){
        total += H*total + string.charCodeAt(i)
    }
    total = total % arr.length;
    return parseInt(total)
}
//散列表存取
function put(key,data){
    var pos = this.betterHash(key);
    this.table[pos] = data
}
function get(key){
    return this.table[this.betterHash(key)]
}

散列函数的选择依赖于键值的数据类型,如果是键是整型,可以以数组的长度对键取余,如果键是随机的整数,散列函数应该更均匀的分布这些键 ,这种散列方式为除留余数法。

7.2 碰撞处理

Hash函数设计的如何巧妙,总有特殊的key导致hash冲突,解决冲突的几个常用方法有:开放定制法,链地址法,公共溢出区法,再散列法

开链法:在常见存储散列过得键值的数组时,通过调用一个函数创建一个新的空数组,将该数组赋给散列表里的每个数组元素,创建一个二维数组

function buildChains(){
    for(let i=0;i<this.table.length;i++){
        this.table[i] = new Array()
    }
}

线性探测法:开放寻址散列,在发生碰撞时,检测下个位置是否为空,如果为空就将数据存入,不为空,继续查找,直到找到空的位置。

7.3 ES6 Map

ES6提供了Map数据结构,类似于对象,但是key可以是任何数据类型,是一种更完善的Hash结构实现。

let m = new Map();
let obj = {'test':'hi'};
m.set(obj,'OK');
m.get(obj) // 'OK'
m.has(obj) //true
m.delete(obj) //true
m.clear() //清空
m.size() //成员数量
//key为对象时,需要保证是同一个对象的引用

遍历的方法有keys(),values(),entries(),forEach()

const map = new Map();
map.set('a','110');map.set('b','220');
for(let key of map.keys()){
    console.log(key)
}//'a' 'b'

for(let val of map.values()){
    console.log(val)
}//110 220

for(let item of map.entries()){
    console.log(item[0],item[1])
}// a 110 b 220

8.集合(set)

集合的特征一个是无序,其次集合中不允许相同的成员存在。

8.1 构造

集合的基本操作有并集,交集,补集

function set(){
    this.dataStore = [];
    this.add = add;
    this.remove = remove;
    this.size = size;
    this.union = union;
    this.intersect = intersect;
    this.subset = subset;
    this.difference = difference;
    this.show = show;
}
//添加
function add(data){
    if(this.dataStore.indexOf(data) < 0){
        this.dataStore.push(data);
        return true
    }
    return false
}
//删除
function remove(data) {  
    var pos = this.dataStore.indexOf(data);   
    if (pos > -1) {   
        this.dataStore.splice(pos,1);   
        return true;  
    }
    return false;  
}
//辅助
function contains(data) {  
    return  this.dataStore.indexOf(data) > -1
}
function size() {    
    return this.dataStore.length; 
}
//union并集
function union(set) {  
    var tempSet = new Set();  
    for (var i = 0; i < this.dataStore.length; ++i) {   
        tempSet.add(this.dataStore[i]);  
    }  
    for (var i = 0; i < set.dataStore.length; ++i) {   
    	if (!tempSet.contains(set.dataStore[i])){      
            tempSet.dataStore.push(set.dataStore[i]);   
    	}  
    }  
    return tempSet; 
}
//intersect交集
function intersect(set) {  
    var tempSet = new Set();  
    for (var i = 0; i < this.dataStore.length; ++i) {   
    	if (set.contains(this.dataStore[i])) {     
            tempSet.add(this.dataStore[i]);   
    	}  
    }  
    return tempSet; 
}
//subset 子集
function subset(set) {  
    if (this.size() > set.size()) {
    	return false;  
    } else {    
    	for each (var member in this.dataStore) {    
    	    if (!set.contains(member)) {      
                return false;    
    	    }    
    	}  
    }  
    return true; 
}
//difference补集
function difference(set) {  
    var tempSet = new Set();  
    for (var i = 0; i < this.dataStore.length; ++i) {   
    	if (!set.contains(this.dataStore[i])) {     
            tempSet.add(this.dataStore[i]);   
    	}  
    }  
    return tempSet; 
}

8.2 ES6 set

事实上ES6提供了set这种数据结构,set函数可以接收一个数组(或类似数组的对象)作为参数,用来初始化

//初始化
let yset = new Set([1,2,3,3]);
[...yset]
//展开,[1,2,3]
let s = new Set('ssiv') 
//'s','i','v'

在set内部,NaN和NaN是相等的,两个对象总是不相等的。

add方法,delete方法,has方法,clear方法,内部遍历for...of...

let mset = new Set();
mset.add(1).add(2).add(2)
//[1,2]
mset.has(1);//true
mset.delete(1);//true
mset.clear();
mset.size // 0

//去重
[...new Set([1,2,2,2,2])]
let arr = Array.from(new Set([1,1,2,3]));
//Array.from()可以将类似数组的对象转换为数
//遍历
let s = new Set([1,2,3,4,5]);
for(let i of s.keys()){
    console.log(i)
}//1 2 3 4 5

for(let i of s.values()){
    console.log(i)
}//1 2 3 4 5

for(let i of s.entries()){
    console.log(i)
}//[1, 1] [2, 2] [3, 3] [4, 4] [5, 5]

let otherSet = s.entries();
otherSet.next().value //[1,1]
otherSet // {2,3,4,5}

交集,并集,差集的实现

//并集
let arr1 = [1,2,3],arr2=[3,4,5];
let a = new Set(arr1), b= new Set(arr2);
let arr3 = [...new Set([...arr1,...arr2])]
//交集
let arr4 = new Set(arr1.filter(x => b.has(x))) //{3}
//差集
let arr5 = new Set(arr1.filter(x=>!b.has(x))) //{1,2}
let arr6 = new Set(arr2.filter(x=>!a.has(x))) //{4,5}
[...arr5,...arr6] //[1,2,4,5]

9.树

树是一种非线性的数据结构,分层存储数据,树由以边连接的节点组成。

二叉树是一种特殊的树,它的子节点不超过两个。

二叉查找树(BST)是一种特殊的二叉树,相对小的值被保存在左节点中,较大的值保存在右节点中,所以查找效率很高。

9.1 构造

function Node(data,left,right){
    this.data = data;
    this.left = left;
    this.right = right;
    this.show = show;
}
function show(){return this.data}
function BST(){
    this.root = null;
    this.insert = insert;
    this.inOrder = inOrder;
}
//插入新节点
function insert(data){
    let n = new Node(data,null,null);
    if(this.root == null){
        this.root = n
    }else{
        let cur = this.root;
        let parent;
        while(true){
            parent = cur;
            if(data< cur.data){
                cur = cur.left;
                if(cur == null){
                    parent.left = n;
                    break
                }
            }else{
                cur = cur.right;
                if(cur == null){
                    parent.right = n;
                    break
                }
            }
        }
    }  
}
//中序遍历,升序访问节点
function inOrder(node){
    if(!(node == null)){
        inOrder(node.left);
        console.log(node.show())
        inOrder(node.right)
    }
}
//先序遍历
function preOrder(node){
    if(!(node == null)){
        console.log(node.show())
        preOrder(node.left);
        preOrder(node.right)
    }
}
//后续遍历
function postOrder(node){
    if(!(node == null)){
        postOrder(node.left);
        postOrder(node.right)
        console.log(node.show())
    }
}
//区别在于打印的位置
//test: var nums = new BST(); nums.insert(23); nums.insert(45); nums.insert(16); nums.insert(37); nums.insert(3); nums.insert(99); nums.insert(22);  inOrder(nums.root);

9.2 应用

BST查找的方法比较简单

//查找最小值,即最左边的节点
function getMin(){
    let cur = this.root;
    while(!(cur.left == null)){
        cur = cur.left
    }
    return cur.data
}
//查找最大值,即最右边的节点
function getMin(){
    let cur = this.root;
    while(!(cur.right == null)){
        cur = cur.right
    }
    return cur.data
}
//查找给定的值,比较当前值,确定向左遍历还是向右遍历
function find(data){
    let cur = this.root;
    while(cur != null){
        if(cur.data == data){
            return cur
        }else if(cur.data < data){
            cur = cur.right
        }else{
            cur = cur.left
        }
    }
    return null
}

参考书籍:数据结构与算法JavaScript描述 (Michael McMillan )