一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情。
1、分析题目
1、聊一下单向链表
要反转单向链表,手下我们要先简单了解一下什么是单向链表?在前端我用到链表的情况并不是很多,所以对链表的理解也不是很深刻,下面简单聊一下我对它的理解。
首先上图
这张图简单的描述出了单向链表的特点,下面再简单聊一下:
- 链表是一种物理结构,类似于数组;不是逻辑结构,和栈、队列不在一个维度上;
- 前端对数组很熟悉,链表和数组也有相似处,但数组需要一段连续的内存空间,但是链表所对应的内存空间却是零散的。
- 数组就像是坐在教室里边的学生,这个时候呢,学生已经将自己的东西放到书桌里边,如果有人要移动位置,该位置和其后边的同学都会搬着桌子向后移动(因为桌子里边有东西,太多,收拾书本不如直接办桌子快呀)
- 链表呢,就像升学后第一次要进入教室的学生,大家都知道自己后边是谁,前边是谁。但是呢,没有做到书桌前,应确立了前后关系。而单向链表就是每个同学只需要知道自己后边的同学就好了。
- 数组和链表都是有序结构:存在前后顺序
- 链表:查询慢O(n),新增和删除快O(1)
- 数组: 查询快O(1),新增和删除慢O(n)
- 这里的新增和删除指的是在数组的非末端进行
- 链表每个节点(相当于数组中的每个元素)的数据结构是{value,next?,prev?}
- 这里边加?表示可能不存在,比如说链表的第一项就不存在prev,链表的最后一项就不存在next;
- 上边的数据结构是双向链表的数据结构,单向链表的数据结构为{value,next?}
2、用代码实现一个单向链表
//定义链表结构
interface ILinkListNode{
value: number,
next?: ILinkListNode
}
//根据输入的数组创建单向链表函数
function createLinkList(arr: number[]): ILinkListNode{
const length = arr.length;
if(arr.length === 0) throw Error('链表不能为空')
//设置当前节点为数组的最后一个元素,并且肯定不存在next
let curNode: ILinkListNode = {
value: arr[length-1]
}
//如果数组只存在一个元素,那么上方创建的curNode就是链表的第一项也是最后一项(不包含null)
if(arr.length === 1) return curNode;
//如果数组包含多个元素,因为最后一项已经被使用,所以从倒数第二项开始
for(let i = arr.length -2; i >= 0; i--){
curNode = {
value: arr[i],
next: curNode
}
}
return curNode;
}
//功能测试
const testArr = [100, 200, 300];
const testLinkList = createLinkList(testArr);
console.log(testLinkList);//{value:100,next:{value:200,next{value:300}}}
//符合预期
2、解题思路
所谓的反转,就是将节点中的next属性的值由只想后边的节点变成指向前边的节点;
代码1
//反转单向链表,返回新的head node
function reverseLinkList(listNode: ILinkListNode): ILinkListNode{
let prevNode: ILinkListNode | undefined = undefined;
let curNode: ILinkListNode | undefined = undefined;
//初始化nextNode在head的位置,并向后移动,直到nextNode不存在变为null结束
let nextNode: ILinkListNode | undefined = listNode;
//以nextNode为主,循环链表
while(nextNode){
//第一个元素删除掉next,防止循环引用
//为什么第一个要删除next,看下方代码,因为head没有prevNode,//如果不删除head的next属性,那么会直接执行整体向后移动,在进入第二次while循环的这个时候prevNode的next属性只想curNode,而curNode的next属性又指向prevNode,循环引用
if(curNode && !prevNode){//当curNode移动道最初始的head时
delete curNode.next
}
//反转指针,反转next
if(curNode && prevNode){//当curNode移动道最初始的head时
curNode.next = prevNode;
}
//整体向后移动
prevNode = curNode;
curNode = nextNode;
nextNode = nextNode?.next;
}
//最后一个,当nextNode为null时,curNode并没有反转,也就是说并没有对next属性赋值
curNode!.next = prevNode;
return curNode!;
}
//功能测试
const reverseList1 = reverseLinkList(testLinkList);
console.log(reverseList1)//{value:300,next:{value:200,next:{value:100}}}
//符合预期
代码2
function reverseLinkList1(listNode: ILinkListNode): ILinkListNode{
let prevNode: ILinkListNode | undefined = undefined;
let curNode: ILinkListNode | undefined = listNode;
let nextNode = curNode?.next;
while(curNode){
if(curNode && !prevNode){
delete curNode.next;
}
if(curNode && prevNode){
curNode.next = prevNode;
}
prevNode = curNode;
curNode = nextNode;
nextNode = nextNode?.next
}
return prevNode!;
}
//功能测试
const testArr2 = [300,400,500,600]
const testLinkList2 = createLinkList(testArr2);
const reverseList2 = reverseLinkList1(testLinkList2)
console.log(reverseList2);//{value:600,next:{value:500,next:{value:400,next:{value:300}}}}
//符合预期
复杂度分析
空间复杂度O(n)
因为是反转链表,所以占用的空间数量级和输入的链表的数量级是相同的,所以是O(n)
时间复杂度O(n)
随着输入的链表发生改变,while循环的次数也会线性发生改变,而且不存在O(n)的内置函数,所以是O(n)
3、总结
在这道题目中首先要基本了解单向链表的结构,其次要注意当curNode对应链表中的curNode的时候,要删除掉curNode.next,避免循环引用。最后提供了两套代码,实际上是相同的思路。代码一是以nextNode为基准向后移动,代码二是以curNode为基准向后移动。返回的都是基准前的那个节点。思路一样,但是会涉及到最后的边缘处理不同,看看哪套代码方便理解。