前言
主要是数据结构与算法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 )