定义一个js函数,反转单向链表

460 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

1、分析题目

1、聊一下单向链表

要反转单向链表,手下我们要先简单了解一下什么是单向链表?在前端我用到链表的情况并不是很多,所以对链表的理解也不是很深刻,下面简单聊一下我对它的理解。
首先上图

1.jpeg 这张图简单的描述出了单向链表的特点,下面再简单聊一下:

  1. 链表是一种物理结构,类似于数组;不是逻辑结构,和栈、队列不在一个维度上;
  2. 前端对数组很熟悉,链表和数组也有相似处,但数组需要一段连续的内存空间,但是链表所对应的内存空间却是零散的。
    • 数组就像是坐在教室里边的学生,这个时候呢,学生已经将自己的东西放到书桌里边,如果有人要移动位置,该位置和其后边的同学都会搬着桌子向后移动(因为桌子里边有东西,太多,收拾书本不如直接办桌子快呀)
    • 链表呢,就像升学后第一次要进入教室的学生,大家都知道自己后边是谁,前边是谁。但是呢,没有做到书桌前,应确立了前后关系。而单向链表就是每个同学只需要知道自己后边的同学就好了。
    1. 数组和链表都是有序结构:存在前后顺序
    2. 链表:查询慢O(n),新增和删除快O(1)
    3. 数组: 查询快O(1),新增和删除慢O(n)
      • 这里的新增和删除指的是在数组的非末端进行
  3. 链表每个节点(相当于数组中的每个元素)的数据结构是{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为基准向后移动。返回的都是基准前的那个节点。思路一样,但是会涉及到最后的边缘处理不同,看看哪套代码方便理解。