眼看春招就要开始了,许多想从事前端行业的人士都开始摩拳擦掌准备面试,然而就现在前端来说,已经不是以前单纯的会写HTML、CSS、JS就行了。本人看了几个前端春招的招聘信息,框架自然不用多说,三大框架怎么也得要求熟练一个或多个。更多招聘要求上都写了了解或熟悉数据结构和算法。今天正好一个学弟问到了我这个问题,他在面试中被要求写一个单向链表,实现在尾部添加一项,在特定位置插入一项,移除一项,返回指定元素对应索引,判断链表是否为空以及返回链表长度这几个方法。他查看了许多资料,大部分是基于后端的,还有部分介绍的不是很清楚。所以本人趁着业务不是忙,献出博客的一血,希望能帮到有需求的人士
数组
说到链表,就不得不提一下数组。要存储多个元素,数组可能是最常用的数据结构。
JS为我们 提供了许多操作数组的方法。数组这种数据结构有这么几个特点:
数组 在内存中是一块连续的区域。这就造成了插入或删除的成本很高,就好比排队,有个人在中间插队,那他这个位置往后的所有人都需要往后移动一个位置;某个人从队伍中离开了,为了保持连续性,那他这个位置往后的所有人都需要向前移动一个位置。
在大多数语言里,数组的大小是固定的且需要提前申请使用空间容易造成浪费及空间不够的情况。同样是排队,某地铁站开辟了一块供500人排队的区域,但是排队人数只有100人,那么剩下400人的位置就浪费了;突然某天来了800人旅游团,上面说了,由于数组是连续的,所以不可能先过500人,再过300人。这就造成了原先的位置就不够用了,就需要重新开辟一个800人的空间。这就引出了另一个数组的问题,就是不利于拓展,在预留空间不够的情况下只能重新定义数组
随机读取效率很高。因为数组是连续的,知道每一个数据的内存地址,可以直接找到对应地址的数据。
链表
相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。但是,链表需要使用指针,因此实现链表时需要特别注意,在数组中是可以直接访问任何位置的任何元素,而要想访问链表中间的一个元素,需要从起点开始迭代列表直到找到所需的元素,假如在一个单向链表你错过了你想查找的内容,那么只能重新从头遍历,因为是单向。双向链表不存在这个问题。链表有这么几个特点
在内存中并不是连续的。就比如10个人坐火车,坐在了不挨着的位置
每个数据都保存了下一个数据的存储地址,通过这个地址去查找下一个数据。第一个人知道第二个人的位置,第二个人知道第三个人的位置…
增加数据和删除数据很容易。 某站又上来一个人X,这个人想坐在A和B中间,那他只需要把自己想要的位置告诉A(让A数据的下一个数据地址指向X),然后从B那里离拿到原来B的地址(让X自己的下一个 数据的地址指向B)就行了。其他人都不用动。
因为不具有随机访问性,所以访问某个位置的数据都要从第一个数据开始访问,然后根据第一个数据保存的下一个数据的地址找到第二个数据,以此类推。 要找到第10个人,也必须从第1个人开始找
总结
数组
- 插入删除效率低
- 可能浪费内存
- 必须要有足够的连续内存空间
- 拓展性差
链表
- 插入删除快
- 内存利用率高
- 拓展灵活
- 必须从第一个开始查找,查找效率低
实现一个单向链表
说了这么多,来实现一个简单的单向链表吧。根据学弟面试要求实现一个包含,在尾部添加一项,在特定位置插入一项,移除一项,返回指定元素对应索引,判断链表是否为空以及返回链表长度这几个方法的单向链表
首先定义一个链表类
function LinkedList () {
...
}
定义一个辅助Node类,Node类表示要加入列表的项。它包含一个val属性,即要添加到列表的值,以及一个next属性,即指向列表中下一个节点项的指针。
function LinkedList () {
functtion Node (val) {
this.val = val
this.next = next
}
...
}
定义链表长度length,头部元素head
function LinkedList () {
functtion Node (val) {
this.val = val
this.next = next
}
let length = 0 // 空链表初始长度为0
let head = null // 空链表没有第一个元素
...
}
定义在尾部添加一项的方法
function LinkedList () {
functtion Node (val) {
this.val = val
this.next = null //下一项指向null,说明没有下一项
}
let length = 0 // 空链表初始长度为0
let head = null // 空链表没有第一个元素
this.push = function(val){ // 形参val为要插入的项
// 创建node项
let node = new Node(val)
// 创建当前节点current,由于单向链表从第一个项开始遍历,所以将第一项head赋值给current
let current = head
// 当第一项head为null时,说明该链表时一个空链表
if (head === null) {
head = node // 将要插入的node项直接作为该链表的第一项
} else { // 该链表不为空
// 循环遍历链表
while (current.next) {
current = current.next
}
// 当前项的下一项next指针为null时证明没有下一项,跳出循环
// 跳出循环后证明已经到达尾部,此时当前项(也是最后一项)的next指针指向插入项
current.next = node
}
length++ // 插入一个新项后使长度递增
}
}
定义在特定位置插入一项的方法
function LinkedList () {
...
// 在任意位置插入
this.insert = function(position, val){ // 形参position为插入位置,val为插入的值
// 首先判断想要插入的位置是否在合理范围内
if (position >= 0 && position <= length) {
// es6语法,创建node插入项,当前项current,前一项previous, 索引index(从0开始)
let [node, current, previous, index] = [new Node(val), head, null, 0]
// 当插入的位置为0 说明在第一项的位置插入
if (position === 0) {
node.next = current // 直接将插入项的下一项next指针指向当前项(第一项)
head = node // 此时node为第一项,所以重新对head进行赋值
} else { // 其他位置插入
// 循环遍历
while (index++ < position){// 当索引值小于想要插入的位置时
// previous=是对想要插入新元素的位置之前一个元素的引用
previous = current
// current变量=对想要插入新元素的位置之后一个元素的引用
current = current.next
}
// 跳出循环后,使前一项的next指针指向插入项
previous.next = node
// 插入项的next指针指向下一项
node.next = current
}
length++
return true // 插入成功返回 true
} else {
return false // 失败返回 false
}
}
}
定义移除一项的方法
function LinkedList () {
...
// 移除
this.removeAt = function(position){// 形参position为删除位置
// 首先判断想要删除的位置是否在合理范围内
if (position >= 0 && position < length){
// es6语法,当前项current,前一项previous, 索引index(从0开始)
let [current, previous, index] = [head, null, 0]
// 当删除的位置为0 说明删除第一项
if (position === 0) {
head = current.next // current = head; head.next = null 所以head=null
} else {
// 循环遍历
while (index++ < position) {
// previous=对想要删除的位置之前一个元素的引用
previous = current
// current变量=对想要删除元素的位置之后一个元素的引用
current = current.next
}
// 跳出循环后,使前一项的next指针指向后一项的next指针,就可以跨过想要删除的项
// 此时当前项就会被丢弃在内存中等待垃圾回收机制回收
// 通俗来说,A,B,C,删除B,使A.next指向B.next,而B.next = C,就相当于A.next指向C
previous.next = current.next
}
length-- // 删除元素 长度减少
return current.val返回删除的元素
} else {
return null // 不符合条件返回null
}
}
}
定义获取索引的方法
function LinkedList () {
...
// 索引
this.indexOf = function (val) {
let [current,index] = [head, 0]
// 循环遍历,如果current存在,判断形参和current的值是否相等,相等返回对应index
// 不相等则index自增,将current的下一项current.next赋值给current
while (current) {
if (val=== current.val) {
return index
}
index++
current = current.next
}
return -1
}
}
定义判断是否为空,链表长度,获取第一个项的方法
function LinkedList () {
...
// 是否为空
this.isEmpty = function(){
return length === 0
}
// 长度
this.size = function(){
return length
}
// 获取第一个元素
this.getHead = function (){
return head
}
}
完整代码
// 链表
function linkedList(){
function Node(val){
this.val= val
this.next = null
}
let head = null
let length = 0
// 尾部追加
this.push = function(val){
let node = new Node(val)
let current = null
if (head === null) {
head = node
} else {
current = head
while (current.next) {
current = current.next
}
current.next = node
}
length++
}
// 在任意位置插入
this.insert = function(position, val){
if (position >= 0 && position < length) {
let [node, current, previous, index] = [new Node(val), head, null, 0]
if (position === 0) {
node.next = current
head = node
} else {
while (index++ < position){
previous = current
current = current.next
}
previous.next = node
node.next = current
}
length++
return true
} else {
return false
}
}
// 移除
this.removeAt = function(position){
if (position >= 0 && position < length){
let [current, previous, index] = [head, null, 0]
if (position === 0) {
head = current.next
} else {
while (index++ < position) {
previous = current
current = current.next
}
previous.next = current.next
}
length--
return current.val
} else {
return null
}
}
// 索引
this.indexOf = function (val) {
let [current,index] = [head, 0]
while (current) {
if (val=== current.val) {
return index
}
index++
current = current.next
}
return -1
}
// 是否为空
this.isEmpty = function(){
return length === 0
}
// 长度
this.size = function(){
return length
}
// 获取第一个元素
this.getHead = function (){
return head
}
}
结语
//实例化后就可以调用方法测试了
let l = new linkedList()
...
到此为止一个简单的单向链表就竣工了。希望可以帮到那些正在学习数据结构的前端人员。虽然前端工作中很少涉及,但是俗话说得好,技多不压身,当能力达到一定层次后,那些所谓寒冬不寒冬的影响也就不大了,安安心心过个东还是问题的。