数据结构是以某种形式将数据组织在一起的集合,它不仅存储数据,还支持访问和处理数据的操作。算法是为求解一个问题需要遵循的、被清楚指定的简单指令的集合。
下面是我总结整理出的关于数据结构与算法的相关内容,如有错误,欢迎交流。本文中对于方法的封装均采用JavaScript语言来实现。
一、数组
JavaScript中已经对数组有了一个非常完善的封装,所以js的数组就是API的调用。此处就不再重复讲了。
补充其他语言里面数组的封装:
(1)常见语言(如Java)的数组不能存放不同类型的数据元素,因此所有在封装时存放在数组里的时Object类型;
(2)常见语言的数组容量不会自动改变(需要进行扩容处理);
(3)中间插入和操作性能低。
使用数组的优点:通过下标值取元素和修改元素的效率特别高
二、栈结构(stack)
栈是一种常见受限的线性结构,其限制性在于访问、插入和删除元素只能在一端进行,这一端被称为栈顶,相对地,另一端被称为栈底。
LIFO(last in first out):表示后进入的元素,第一个弹出栈空间。类似于一挪盘子的使用,总是后放上去的先被使用。
向一个栈插入一个新元素又称作进栈、入栈或压栈(push),它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素。
从一个栈删除元素又称作出栈或退栈(pop),它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

下面来看一道经典面试题:

正确答案是C:
A答案:65进栈,5出栈,4进栈出栈,3进栈出站,6出栈,21进栈,1出栈,2出栈
B答案:654进栈,4出栈,5出栈,3进栈出栈,2进栈出栈,1进栈出栈,6出栈
D答案:65432建或者拿,2出栈,3出栈,4出栈,1进栈出栈,5出栈,6出栈
- 栈的封装及常见操作:
- 封装栈类:
function Stack(){
//栈中的属性
this.items=[];
//栈的相关操作
//栈的使用
var s=new Stack();
}
- 元素入栈的方法(push):
Stack.prototype.push=function(element){
this.items.push(element);
};
- 元素出栈(pop)的方法:
Stack.prototype.pop=function(){
return this.items.pop();
};
- 查看栈顶元素:
Stack.prototype.peek=function(){
return this.items[this.items.length-1];
};
- 判断栈是否为空:
Stack.prototype.isEmpty=function(){
return this.items.length==0;
};
- 获取栈中元素的个数:
Stack.prototype.size=function(){
return this.items.length;
};
- toString方法:
Stack.prototype.toString=function(){
var resultString='';
for(var i=0;i<this.items.length;i++){
resultString+=this.items[i]+'';
}
return resultString;
};
栈的应用:十进制数转二进制
function decTobin(number){
var stack=new Stack(); //定义栈对象
while(number>0){ //循环操作
//获取余数并压入栈中
stack.push(number%2);
//获取整除后的结果,作为下一次的运行数字
number=Math.floor(number/2);
}
//从栈中取出0和1
var binnum='';
while(!stack.isEmpty()){
binnum+=stack.pop();
}
return binnum;
}
三、队列结构(queue)
队列也是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,进行插入操作的端称为队尾,进行删除操作的端称为队头。

队列的常见操作与栈的操作类似,不同处在于入队列是enqueue,出队列是dequeue方法,查看队列中第一个元素时front方法。其方法的封装与栈类似,注意队列的特点是FIFO就可以了,此处不再赘述。
- 队列的应用:击鼓传花游戏的实现
游戏规则:几个人围成一圈玩一个游戏,开始数数,数到某个数字的人自动淘汰,最后剩下的人获胜。
function passFlower(nameList,num){
//1.创建一个队列结构
var queue=new Queue();
//2.将所有人加入到队列里面
for(var i=0;i<nameList.length;i++){
queue.enqueue(nameList[i]);
}
//3.开始数数
while(queue.size()>1) {
//3.1不是num的人,重新加入到队列的末尾位置
for (var k = 0; k < num-1; k++) {
queue.enqueue(queue.dequeue());
}
//3.2num对应的人,直接从队列中删除
queue.dequeue();
}
//4.获取剩下的那个人
var endname=queue.front();
console.log("最后的赢家是:"+endname);
return nameList.indexOf(endname)
}
队列中还有一个特殊的优先级队列,它不遵从先进先出(FIFO)的原则,在优先队列中,元素都被赋予优先级。当访问元素的时候,具有最高优先级的元素最先被删除。优先队列在生活中的应用还是比较多的,比如医院的急症室为病人赋予优先级,具有最高优先级的病人最先得到治疗。
四、链表结构
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点组成,这些节点不必在内存中相连。每个节点由数据部分Data和链部分Next,Next指向下一个节点,这样当添加或者删除时,只需要改变相关节点的Next的指向,效率很高。
单链表的结构示意:


- 相比于数组,链表的优势:
(1)内存空间不必是连续的,可以充分利用计算机的内存,实现灵活的内存动态管理
(2)链表不必在创造时就确定大小,并且大小可以无限延伸下去
(3)链表在插入和删除数据时,时间复杂度可以达到O(1),比数组效率高很多 - 链表相比于数组的缺点:
(1) 访问任何一个位置的元素,都需要从头开始访问(无法跳过第一个元素访问任何一个元素)
(2) 无法通过下标直接访问元素,需要从头一个个访问直到找到对应的元素。 - 链表的封装:

- 链表中追加元素的方法:
LinkedList.prototype.append=function(data) {
//1.1创建一个新的节点
var newnode = new Node(data);
//1.2判断添加的是不是第一个节点
if (this.length == 0) { //是第一个节点,则head指向新添加的节点
this.head = newnode;
}
else { //不是第一个节点
var current = this.head; //current是head指向的节点(即第一个节点)
while (current.next) { //判断节点的next指针是否为空
current = current.next;
}
current.next = newnode;
}
//1.3改变length长度
this.length += 1;
};
- toString方法:
LinkedList.prototype.toString=function(){
//2.1定义变量
var current=this.head;
var listString="";
//2.2循环获取一个个的节点
while(current){
listString+=current.data+" ";
current=current.next;
}
return listString;
}
- 插入元素的insert方法:
LinkedList.prototype.insert=function(position,data){
//3.1判断越界的情况
if(position<0||position>this.length){
return false;
}
//3.2创建新的节点
var newnode=new Node(data);
//3.3判断节点插入的位置
if(position==0){ //在第一个节点前插入元素
newnode.next=this.head;
this.head=newnode;
}
//插入的位置没有在第一个节点前面
else{
var current=this.head;
var preious=null;
var index=0;
while(index<position){
preious=current;
current=current.next;
index++;
}
newnode.next=current;
preious.next=newnode;
}
//3.4改变链表的长度
this.length+=1;
return true;
}
- get获取对应位置元素的方法
LinkedList.prototype.get=function(position){
//4.1判断越界
if(position<0||position>this.length-1){
return null;
}
//4.2获取对应位置的data
var current=this.head;
var index=0;
while(index<position){
current=current.next;
index++;
}
return current.data;
}
除此之外,链表中还有:indexOf方法(根据给定值查找,返回所查找元素的索引)、updata修改给定位置数据的方法、removeAt根据给定位置删除元素的方法、remove删除数据方法、isEmpty方法以及size方法等,思路与上面类似。
双向链表
双向链表既可以从头遍历到尾,也可以从尾遍历到头,一个节点既有向前连接的引用,也有一个向后连接的引用。主要是节点中包含两个指针部分,一个指向前驱元,一个指向后继元。

双向链表的操作与单向链表类似,添加了两个方法:
- forwordString:返回正向遍历的节点字符串形式;
DoublyLinkList.prototype.forwardString=function(){
//定义变量
var current=this.tail;
var resultString="";
//从后往前依次遍历获取每个节点
while(current){
resultString+=current.data+" ";
current=current.prev;
}
return resultString;
};
- backwordString:返回反向遍历的节点字符串形式。
DoublyLinkList.prototype.backwardString=function(){
//定义变量
var current=this.head;
var resultString="";
//从前往后遍历,获取每一个节点
while(current){
resultString+=current.data+" ";
current=current.next;
}
return resultString;
};
五、集合
(1) 集合通常是由一组无序的、不能重复的元素构成;
(2) 集合是一种特殊的数组
特殊之处在于里面的元素没有顺序也不能重复;没有顺序意味着不能通过下标值进行访问,不能重复意味着相同的对象在集合中只能存在一份。
- 集合里常见的操作方法:

- 集合间的操作:
- 并集:对于给定的两个集合,返回一个包含两个集合中所有元素的集合;
- 交集:对于给定的两个集合,返回一个包含两个集合中共有元素的集合;
- 差集:对于给定的两个集合,返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合;
- 子集:验证一个给定集合是否是另一集合的子集;
六、字典
字典的主要特征:以键值对来存储,键和值是一一对应的关系;
字典中的键不允许重复切无序,但值可以重复;
在JavaScript中可以用对象来代替字典(对象也是键、值存储的)
七、哈希表
- 哈希表介绍:
哈希表通常是基于数组实现的,但相对于数组也有很多优势:
(1) 它可以提供非常快速的插入、删除、查找操作
(2) 无论多少数据,插入和删除值需要接近常亮的时间:即O(1)时间级。实际上只需要几个机器指令即可完成,
(3) 哈希表的速度比树还要快,基本可以瞬间查找到想要的元素
(4) 哈希表相对于树来说编码要容易的多。
哈希表相对于数组的一些不足
(1) 哈希表中的数据是没有顺序的,所以不能以固定的方式(比如从小到大)来遍历表中的元素;
(2) 通常情况下表中的key是不可以重复的,不能放置相同的key,用于保存不同的元素。 - 哈希表的一些概念:
哈希化:将大数字转化成数组范围内下标的过程,称之为哈希化
哈希函数:将单词转成大数字,大数字在进行哈希化的代码实现放在一个函数中,这个函数成为哈希函数。
哈希表:最终将数据插入到的这个数组,对整个结构的封装,我们就称之为一个哈希表。 - 哈希表中解决冲突的方法:
哈希化之后转化来的数组下标可能会产生冲突,所以需要解决冲突
解决冲突一般有两种方法:链地址法和开放地址法
(1) 链地址法(拉链法):
链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,而是一个链条(链条一般使用数组或者链表来实现),当查询时,先根据哈希化后的下标值找到对应的位置,再取出链表,依次查询要寻找的数据。
(2) 开放地址法:
寻找空白的单元格来添加重复的数字
寻找方法:线性探测法、二次探测、再哈希法 - 哈希化的效率:
优秀的哈希函数应该尽可能的将元素映射到不同的位置,让元素在哈希表中均匀的分布,为了实现均匀分布,我们需要在使用常量的地方,尽可能的使用质数。
质数的使用:
(1)哈希表的长度
(2)N次幂的底数 - 判断一个数是不是质数的方法:

八、树结构
树是由n(n>=1)个有限节点组成一个具有层次关系的集合。它具有以下特点:每个节点有零个或多个子节点;没有父节点的节点称为 根 节点;每一个非根节点有且只有一个 父节点 ;除了根节点外,每个子节点可以分为多个不相交的子树。

- 二叉树的基本概念:
1.定义:二叉树是每个节点最多有两棵子树的树结构。通常子树被称作“左子树”和“右子树”。二叉树常被用于实现二叉查找树。
2.相关性质:
(1)二叉树的每个结点至多只有2棵子树(不存在度大于2的结点),二叉树的子树有左右之分,顺序不能颠倒。
(2)叉树的第i层至多有2^(i-1) 个结点;深度为k的二叉树至多有2^k -1个结点。
(3)在二叉树中,除了最下一层的叶节点以外,每层节点都有两个子节点,就构成了满二叉树。如下图:

(4)除二叉树最后一层外,其他各层的节点数都达到最大个数;最后一层从左到右的叶节点连续存在,只缺右侧若干节点,则这棵树称为完美二叉树,完美二叉树是特殊的完全二叉树。如下图:

3.二叉搜索树:
(BST,也叫二叉排序树,二叉查找树 Binary Search Tree)
(1)二叉搜索树是一颗二叉树,可以为空
(2)如果不为空,满足以下性质:
3.2.1非空左子树的所有键值小于其根节点的键值
3.2.2非空右子树的所有键值大于其根节点的键值
3.2.3左右子树本身也都是二叉搜索树
特点:相对较小值总是保存在左节点上,相对较大值总是保存在右节点上。
4.二叉搜索树的三种遍历
(1)先序遍历
访问根节点 --> 先序遍历其左子树 --> 先序遍历其右子树
(2)中序遍历
中序遍历其左子树 --> 访问根节点 --> 中序遍历其右子树
(3)后序遍历
后序遍历其左子树 --> 后序遍历其右子树 -->访问根节点
如:

先序遍历的结果:A B D H I E C F G J K
中序遍历的结果:H D I B E A F C J K G
后序遍历的结果:H I D E B F K J G C A - 先序遍历
BinarySearchTree.prototype.preOrderTraversal=function(hander){
this.preOrderTraversalNode(this.root,hander);
};
BinarySearchTree.prototype.preOrderTraversalNode=function(node,hander){
if(node!=null){
//处理经过的节点
hander(node.key);
//查找经过节点的左子节点
this.preOrderTraversalNode(node.left,hander);
//查找经过节点的右子节点
this.preOrderTraversalNode(node.right,hander);
}
};
- 中序遍历
BinarySearchTree.prototype.midOrderTraversal=function(hander){
this.midOrderTraversalNode(this.root,hander);
};
BinarySearchTree.prototype.midOrderTraversalNode=function(node,hander){
if(node!=null){
//中序遍历其左子节点
this.midOrderTraversalNode(node.left,hander);
//处理根节点
hander(node.key);
//中序遍历右子节点
this.midOrderTraversalNode(node.right,hander);
}
};
- 后序遍历
BinarySearchTree.prototype.postOrderTraversal=function(hander){
this.postOrderTraversalNode(this.root,hander)
};
BinarySearchTree.prototype.postOrderTraversalNode=function(node,hander){
if(node!=null){
//后续遍历左子节点
this.postOrderTraversalNode(node.left,hander);
//后续遍历右子节点
this.postOrderTraversalNode(node.right,hander);
//处理节点
hander(node.key);
}
};
- 二叉搜索树的简单实现
function BinarySearchTree() {
//封装一个内部的类
function Node(key) {
this.key = key;
this.left = null;
this.right = null;
}
//属性
this.root = null;
//插入数据的方法
BinarySearchTree.prototype.insert = function (key) {
//根据key创建新节点
var newNode = new Node(key);
//判断根节点是否有值
if (this.root == null) {
this.root = newNode;
} else {
//调用递归方法来判断新节点插入的位置
this.insertNode(this.root, newNode);
}
};
BinarySearchTree.prototype.insertNode = function (node, newNode) {
if (node.key < newNode.key) { //向右查找
if (node.right == null) {
node.right = newNode;
} else {
this.insertNode(node.right, newNode);
}
} else { //向左查找
if (node.left == null) {
node.left = newNode;
} else {
this.insertNode(node.left, newNode);
}
}
};
//寻找最大最小值的方法
BinarySearchTree.prototype.max = function () {
//获取根节点
var node = this.root;
//以次向右不断地查找,直到节点为null
var key = null;
while (node != null) {
key = node.key;
node = node.right
}
return key;
};
BinarySearchTree.prototype.min = function () {
//获取根节点
var node = this.root;
//以次向左不断地查找,直到节点为null
var key = null;
while (node != null) {
key = node.key;
node = node.left;
}
return key;
};
//查找值的方法
BinarySearchTree.prototype.search = function (key) {
var node = this.root;
while (node != null) {
if (key < node.key) {
node = node.left;
} else if (key > node.key) {
node = node.right;
}
else {
return true;
}
}
return false;
}
}
5.二叉树中删除节点 比较复杂,所以单独分析:
要在二叉查找树中删除一个元素,首先需要定位包含该元素的节点,以及它的父节点。假设current指向二叉查找树中包含该元素的节点,而parent指向current节点的父节点。删除对应的元素一共有以下几种情况:
(1)删除的是叶子节点(没有子节点)
这种情况比较简单,我们需要检测 current 的 left 和 right 是否都为 null,都为null之后还要检测一个东西,就是current是否是根,如果只有单独的一个根,直接删除即可。否则就把父节点的 left 或者 right 字段设置为 null即可。
BinarySearchTree.prototype.remove=function(key){
//寻找要删除的元素
//定义变量,存储一些信息
var current=this.root; //存储当前节点
var parent=null; //存当前节点的父节点元素
var isleftchild=true; //判断是左子节点还是右子节点
//开始寻找要删除的节点
while(current.key!=key){
parent=current;
if(current.key<key){
isleftchild=true;
current=current.left;
}
else{
isleftchild=false;
current=current.right;
}
//已经找到了最后还是没有找到
if(current==null) return false;
}
//根据对应的情况删除节点
//找到了current==key
//删除的节点是叶子结点(没有子节点)
if(current.left==null&¤t.right==null){
if(current==this.root){
this.root=null;
}
else if(isleftchild){
parent.left=null;
}
else{
current.right=null;
}
}
(2)要删除的节点只有一个子节点
要从三者之间:爷爷–自己–儿子,将自己(current)剪断,让爷爷直接连接儿子即可。这个过程要求改变父节点的left或者right,指向要删除节点的子节点。当然,这个过程中还要考虑current是否就是根。
else if(current.right==null){
if(current==this.root){
this.root=current.left;
}
else if(isleftchild){
parent.left=current.left;
}
else{
parent.right=current.left;
}
}
else if(current.left==null){
if(current==this.root){
this.root=current.left;
}
else if(isleftchild){
parent.left=current.right;
}
else{
parent.right=current.right;
}
}
(3)要删除的节点有两个子节点
如果要删除的元素有两个子节点,甚至子节点还有子节点,这种情况下,我们需要从子节点中找到一个节点,来替换当前的节点。这个用来替换的节点应该是current节点下所有节点中最接近current的。比current小一点点的节点,一定是current左子树的最大值,称为前驱。比current大一点点的节点,一定是current右子树的最小值,称为后继。
所以做删除操作时,要先找到节点的前驱或者后继,此处我们以找后继为例来说明。
else{
//获取后继节点
var successor=this.getSuccessor(current);
//判断是否根节点
if(current==this.root){
this.root=successor;
}
else if(isleftchild){
parent.left=successor;
}
else{
parent.right=successor;
}
//将删除节点的左子树=current.left
successor.left=current.left;
}
};
//找后继节点的方法
BinarySearchTree.prototype.getSuccessor=function(delNode){
//定义变量,保存接收的后继
var successor=delNode;
var current=delNode.right;
var successorParent=delNode;
//循环查找
while(current!=null){
successorParent=successor;
successor=current;
current=current.left;
}
//判断寻找的后继节点是否直接就是delNode的right节点
if(successor!=delNode.right){
successorParent.left=successor.right;
successor.right=delNode.right;
}
return successor;
}
6.二叉搜索树的优点和缺陷:
优点:可以快速的找到给定关键字的数据项,并且快速的删除或插入数据项 。
缺陷:插入连续数据后,有可能会产生非平衡树
7.二叉平衡树:
(1)AVL树是最先发明的自平衡二叉查找树算法。在AVL中任何节点的两个儿子子树的高度最大差别为1,所以它也被称为高度平衡树,n个结点的AVL树最大深度约1.44log2n。查找、插入和删除在平均和最坏情况下都是O(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。
(2)红黑树是平衡二叉树的一种,它保证在最坏情况下基本动态集合操作的事件复杂度为O(log n)。红黑树和平衡二叉树区别如下:1) 红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。2) 平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。
8.红黑树的规则:
(1)节点是红色或黑色;
(2)根节点是黑色;
(3)每个叶子节点都是黑色的空节点(NIL节点);
(4)每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点);
(5)从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
九、图结构
图是一种较线性表和树更为复杂的数据结构,在线性表中,数据元素之间仅有线性关系,在树形结构中,数据元素之间有着明显的层次关系,而在图形结构中,节点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。
- 图的表示方法:
(1)邻接矩阵
让每个节点和一个整数相关联,该整数作为数组的下标值,用一个二维数组来表示顶点之间的连接。

在二维数组中 ,0表示没有连线,1表示有连线;
通过这个二维数组,可很快的找到一个顶点和哪些顶点有连线。
(2)邻接表
邻接表由图中每个顶点以及和顶点相邻的顶点列表组成。
这个列表有很多种方式来存储:数组/链表/字典(哈希表)都可以。

- 图的遍历:
图的遍历意味着需要将图中每个顶点访问一次,并且不能有重复的访问。
有两种算法可以对图进行遍历,两种遍历方法都需要明确指定第一个被访问的顶点。
(1) 广度优先搜索(Breadth-First Search,简称BFS):基于队列,入队列的顶点先被探索。
(2) 深度优先搜索(Depth-First Search,简称DFS):基于栈或使用递归,通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问。
为了记录顶点是否被访问过,我们使用三种颜色来反应他们的状态
白色:表示该顶点还没有被访问;
灰色:表示该顶点被访问过,但未被探索过;
黑色:表示该顶点被访问过且完全被探索过。
本文详细介绍了JavaScript中数据结构与算法的应用,包括数组、栈、队列、链表、集合、字典、哈希表和树结构。讨论了各种数据结构的特点、操作方法及其在实际问题中的应用,如栈的LIFO特性、队列的FIFO原则、链表的高效插入删除等。同时,还探讨了哈希表的高效查找和解决冲突的策略,以及二叉树和图结构的概念与遍历方法。文章旨在帮助读者深入理解数据结构与算法在JavaScript编程中的重要性。
353

被折叠的 条评论
为什么被折叠?



