1、数组
1.1、认识数组
数组对应的应为是array,是有限个相同类型的变量所组成的有序集合,数组中的每一个变量都被称为元素,数组是最简单、最为常用的数据结构。
正如队列中的士兵都拥有一个编号,数组也有一个对应的下标,数组在内存中存储,因此可以很好的实现逻辑上的顺序表
内存是由一个个连续的内存单元组成,每一个内存单元都有自己的地址,在这些内存单元中,有些被其他数据占用了,有些是空闲的。数组汇总的每一个元素都存储在内存单元中,并且紧密排列,也就是一个挨着一个。
1.2、数组的基本操作
1、对于数组来说,读取数组最为简单,由于数组在内存中顺序存储,所以只要给一个数组下标,就可以读取到对应的数组元素。假设有一个名称为array的数组,如果单独下标为5的元素,就写作array[5]。
2、更新元素 要把数组中某一个元素的值替换成一个新值,直接利用数组下标,就可以把新值赋给该元素。简单的代码示例如下:
let arr1= [3,1,2,5,4,9,7,2];
arr1[5] = 1;
console.log(arr1[5]);
结合上一篇文章,直接操作,数组读取和更新元素的时间复杂度都是O(1)。
3、插入元素 在介绍插入数组元素的操作之前,我们需要补充一个概念,那就是数组的实际元素数量有可能小于数组的长度,js中也是存在 empty的可能,因为插入数组元素存在3种情况
- 尾部插入
- 中间插入
- 超范围插入
尾部插入最为简单,直接把元素放在数组尾部的空闲位置即可,等同于元素的更新。
中间插入稍微复杂,由于数组中每一个元素都有固定的下标,所以需要把插入位置及之后的元素整体往后移动,再把要插入的元素当道对应的数组位置上。如果插入元素的时候,超过了元素本身的长度,则需要给出提醒.
超范围插入指的是插入元素会超过数组本身的长度,当然 js会直接自动帮我们扩容,这是语言本身决定的,对应的概念还是要了解一下的。
4、数组的删除 与插入相反,需要去除该元素后,而该元素后面的元素整体往前移动。
总结一下,插入操作,数组扩容的复杂度是O(n),插入并且东的时间复杂度也是O(n),综合起来插入操作的时间复杂度是O(n)。删除操作只涉及元素一定,时间复杂度也是O(n)。显然,数组在读取元素有着非常高效的能力,只要给出下标即可。而劣势体现在插入和删除元素方面,由于数组元素连续紧密的存储,每次插入和删除都会导致大量元素的被迫移动,影响效率。
2、链表
影视作品中,有一种职业叫地下工作者,知道上级姓名住址,也知道下级的姓名住址,但是这个人的上级和下级却互相不知道,借助这种单线联络的方式,灵活隐秘的传递着各种重要的信息。在计算机科学领域里,有一种数据结构也恰恰具备这样的特征,这种数据结构就是链表。
链表是一种在物理上非连续、非顺序的数据结构,由若干节点node所组成。单项列表的每个节点包括两个部分,一部分存放数据的变量data,另一部分是指向下一个节点的指针 next 。
class Node {
let data;
Node next;
}
链表的第1个节点被称为头节点,最后1个节点被称为尾节点,尾节点的next指针指向空。对于链表其中一个节点我们只能根据节点A的next指针找到该节点的下一个节点B,再根据B的next指针找到下一个节点C。
双向链表
双向链表比单项链表复杂一些,他的每一个节点除了拥有data和next指针,还拥有指向前置接点的prev指针。
前文提到数组的存储方式是顺序存储,儿链表则是随机存储,存储单元采用了见缝插针的方式,依靠next指针关联起来,可以灵活有效的利用零散的碎片空间。
链表的基本操作
1、查找节点 在查找元素时,链表不像数组那样可以通过下标快速进行定位,只能从头节点开始向后一个一个节点逐一查找。先定位到头节点,根据next指针,依次定位,时间复杂度为O(n); 2、替换节点 如果不考虑查找节点的过程,链表的更新过程回想数组那样简单,直接将旧数据替换即可,时间复杂度O(1); 3、插入节点 与数组类似分为
- 尾部插入
- 中间插入
- 头部插入
尾部插入,把最后一个节点的next指针指向新插入的节点即可。
头部插入,分为两个步骤:1、把新节点的next指针指向原先的头节点,2、把新节点变为链表的头节点
中间插入,同样分为两个步骤。 第1步,新节点的next指针,指向插入位置的节点。第2步,插入位置前置节点的next指针,指向新节点。
4、删除元素 链表的删除操作同样分为3种情况。
- 尾部删除
- 头部删除
- 中间删除
尾部删除,是最简单的情况,把倒数第2个节点的next指针指向空即可。头部删除,也很简单,把链表的头节点设为原先头节点的next指针指向的节点即可。中间删除也比较简单,把要删除节点的前置节点的next指针,指向要删除节点的next指针指向的节点。
综上对比可以看出,数组的优势在于能够快速定位元素,对于读操作多、写操作少的场景来说,用数组更合适一些,链表的优势在于能够灵活地进行插入和删除操作,如果需要在尾部频繁插入、删除元素,用链表更合适一些。
栈和队列
栈和队列。这两者都属于逻辑结构,它们的物理实现既可以利用数组,也可以利用链表来完成。
栈
假如有一个又细又长的圆筒,圆筒一端封闭,另一端开口。往圆筒里放入乒乓球,先放入的靠近圆筒底部,后放入的靠近圆筒入口。
如果想要取出乒乓球,只能按照和放入顺序相反的顺序来取,先取出后放入的。栈是一种线性数据结构,只能先入后出,最早进入的元素存放的位置叫做栈底,最后进入的元素存放的位置叫做栈顶。
栈的基本操作
1、入栈
入栈操作(push)就是把新元素放入栈中,只允许从栈顶一侧放入元素,新元素的位置将会成为新的栈顶这里以数组为例。
2、出栈
显然,入栈与出栈的时间复杂度都是O(1)。
队列
假如公路上有一条单行隧道,所有通过隧道的车辆只允许从隧道入口驶入,从隧道出口驶出,不允许逆行。因此,要想让车辆驶出隧道,只能按照它们驶入隧道的顺序,先驶入的车辆先驶出,后驶入的车辆后驶出,任何车辆都无法跳过它前面的车辆提前驶出。 队列是一种线性数据结构,它的特征与行使车辆相似,先入先出,队列的出口端叫做队头,队列的入口端叫做队尾。
与栈类似,队列这种数据结构既可以用数组来实现,也可以用链表实现,用数组实现时,为了入队操作的方便,把队尾位置规定为最后入队元素的下一个位置。
队列的操作,包含入队出队,遵守先进后出的规则,与栈类似不在赘述。
聪明的你可能发现,如果不断地出队入队,队头左边空间就会失去作用,这个时候循环队列就发挥了作用,代码如下:
//循环队列
function entry(element) {
if (index + 1 % arr.length == front) {
throw new Error('数组满了')
}
arr[index] = element
}
function test(element) {
entry(element,arr,index,front);
}
栈和队列的应用
1、栈 栈的输出顺序和输入顺序相反,所以栈通常用于对历史的回溯,如实现递归的逻辑,就可以用栈来代替
2、双端队列 双端队列这种数据结构,可以说综合了栈和队列的优点,对双端队列来说,从队头一端可以入队或出队,从队尾一端也可以入队或出队。
3、优先队列 还有一种队列,它遵循的不是先入先出,而是谁的优先级最高,谁先出队。
散列表
散列表也叫作哈希表 (hash table),这种数据结构提供了键(Key)和值(Value) 的映射关系。只要给出一个Key,就可以高效查找到它所匹配的Value,时间复杂度接近于O(1)。
从前面讲述的内容可以知道,数组的查询效率最高,根据下标即可查询,时间复杂度为O(1),散列表可以根据key查询到结果,但是由于可以是以字符串为主,所以我们需要一个“中转站”,通过某种方式,把key和数组下标进行转换,这个中转站就叫做哈希函数。
getHashCode:function(str,caseSensitive){
if(!caseSensitive){
str = str.toLowerCase();
}
// 1315423911=b'1001110011001111100011010100111'
var hash = 1315423911,i,ch;
for (i = str.length - 1; i >= 0; i--) {
ch = str.charCodeAt(i);
hash ^= ((hash << 5) + ch + (hash >> 2));
}
return (hash & 0x7FFFFFFF);
}
散列表的读写操作
1、写操作 写操作就是在散列表中插入新的键值对,如调用hashMap.put("002931", "王五"),意思是插入一组Key为002931、Value为王五的键值对。
分为两步:
第1步,通过哈希函数,把Key转化成数组下标5。
第2步,如果数组下标5对应的位置没有元素,就把这个Entry填充到数组下标5的位置。
当时由于数组长度有限,不同的key通过哈希函数获得的下标是相同的,这种情况就叫哈希冲突,那么如何解决呢?
一种是开放寻址法,一种是链表法。开放寻址法的原理很简单,当一个Key通过哈希函数获得对应的数组下标已被占用时,我们可以“另谋高就”,寻找下一个空档位置。链表法,HashMap数组的每一个元素不仅是一个Entry对象,还是一个链表的头节点。每一个Entry对象通过next指针指向它的下一个Entry节点。当新来的Entry映射到与之冲突的数组位置时,只需要插入到对应的链表中即。
2、读操作
读操作就是通过给定的Key,在散列表中查找对应的Value。例如调用 hashMap.get("002936"),意思是查找Key为002936的Entry在散列表中所对应的值。具体该怎么做呢?下面以链表法为例来讲一下。第1步,通过哈希函数,把Key转化成数组下标2。第2步,找到数组下标2所对应的元素,如果这个元素的Key是002936,那么就找到了;如果这个Key不是002936也没关系,由于数组的每个元素都与一个链表对应,我们可以顺着链表慢慢往下找,看看能否找到与Key相匹配的节点。