概述
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
链表是由结点构成,head指针指向第一个成为表头结点,而终止于最后一个指向NULL的指针。
类型分为单向、双向、循环链表等
- 单向链表,特点是链表的链接方向是单向的。只能从头到尾遍历,只能找到后继无法找到前驱,增加删除节点简单,遍历时候不会死循环。
- 双向链表,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
在查找元素可以反向查找前缀结点,一定程度上提升了查找数据元素的速度,但需要记录前缀结点,增加了额外的内存空间开销。 - 循环链表,它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。
在遍历的时候可以从任意结点开始,增加了遍历的灵活性。
链表 VS 数组:
- 删除、插入操作
由于链表存储非连续的,删除、插入操作时不需要改变内存的地址,只需要修改结点的信息即可(包括指针指向,结点值)。而数组则需要搬移其他的元素(对最后一个元素插入或删除时才比较快)。所以链表性能上优于数组。如果数组是有序的,数组元素的删除操作就不一定比链表慢了。 - 查找操作
数组大小固定不适合动态存储、动态添加,内存为一连续的地址,可随机访问,查询较快。而链表大小可变,扩展性强,只能顺着指针的方向查询,速度较慢。
链表实现
有以下函数:
- append 添加一个新的元素
- insert 在指定位置插入一个元素
- remove 删除指定位置的节点
- removeHead 删除首节点
- removeTail 删除尾节点
- indexOf 返回指定元素的索引
- get 返回指定索引位置的元素
- head 返回首节点
- tail 返回尾节点
- length 返回链表长度
- isEmpty 判断链表是否为空
- clear 清空链表
- print 打印整个链表
function LinkList(){
// 定义节点
var Node = function(data){
this.data = data
this.next = null
}
// 长度
var length = 0
// 头节点
var head = null
// 尾节点
var tail = null
// 添加一个新元素
this.append = function(data){
// 创建新节点
var node = new Node(data)
// 如果是空链表,如果不为空则tail.next = node, 并让tail指向node
if(head==null){
head = node
tail = head
}else{
// 尾节点指向新创建的节点
tail.next = node
// tail指向链表的最后一个节点
tail = node
}
// 长度加1
length += 1
return true
}
// 返回链表大小
this.length = function(){
return length
}
// 获得指定位置的节点
var getNode = function(index){
if(index < 0 || index >= length){
return null
}
var currentNode = head
var nodeIndex = index
while(nodeIndex > 0){
currentNode = currentNode.next
}
return currentNode
}
// 在指定位置插入新的元素
this.insert = function(index, data){
// index == length,说明是在尾节点的后面新增,直接调用append方法即可
if(index == length){
return this.append(data)
}else if(index > length || index < 0){
// index范围错误
return false
}else{
var newNode = new Node(data)
if(index == 0){
// 如果在头节点前面插入,新的节点就变成了头节点
newNode.next= head
head = newNode
}else{
// 要插入的位置是index,找到索引为index-1的节点,然后进行连接
var preNode = getNode(index-1)
newNode.next = preNode.next
preNode.next = newNode
}
length += 1
return true
}
}
// 删除指定位置的节点
this.remove = function(index){
// 参数不合法,如果index< 或者 index>=length,索引都是错误的,返回null
if(index < 0 || index >= length){
return null
}else{
var deleteNode = null
// 如果index==0,删除的是头节点,只需要执行head = head.next就可以把头节点删除。
if(index == 0){
// head指向下一个节点
deleteNode = head
head = head.next
// 如果head == null,说明之前链表只有一个节点
if(!head){
tail = null
}
}
// 如果index > 0,那么就找到索引为index-1的节点,利用这个节点将索引为index的节点删除
else{
// 找到索引为index-1的节点
var preNode = getNode(index-1)
deleteNode = preNode.next
preNode.next = preNode.next.next
// 删除节点时,如果被删除的节点是尾节点,tail要指向新的尾节点
if(deleteNode.next==null){
tail = preNode
}
}
length -= 1
deleteNode.next = null
return deleteNode.data
}
}
// 删除尾节点
this.removeTail = function(){
return this.remove(length-1)
}
// 删除头节点
this.removeHead = function(){
return this.remove(0)
}
// 返回指定位置节点的值
this.get = function(index){
var node = getNode(index)
if(node){
return node.data
}
return null
}
// 返回链表头节点的值
this.head = function(){
return this.get(0)
}
// 返回链表尾节点的值
this.tail = function(){
return this.get(length-1)
}
// 返回指定元素的索引,如果没有,返回-1。有多个相同元素,返回第一个
this.indexOf = function(data){
var index = -1
var currentNode = head
while(currentNode){
index += 1
if(currentNode.data == data){
return index
}else{
currentNode = currentNode.next
}
}
return -1
}
// 输出链表
this.print = function(){
var currentNode = head
var strLink = ''
while(currentNode){
strLink += currentNode.data.toString() + ' ->'
currentNode = currentNode.next;
}
strLink += 'null'
console.log(strLink)
console.log('长度为'+ length.toString())
}
// isEmpty
this.isEmpty = function(){
return length == 0
}
// 清空链表
this.clear = function(){
head = null
tail = null
length = 0
}
}
题型
环形链表
思路
快慢指针。定义2个指针,一个慢指针、一个慢指针,并且一开始慢指针指向head节点,快指针指向head.next节点,然后快指针每次向前移动2步,慢指针每次向前移动1步,开始遍历链表。如果链表有环,那么快慢指针一定会相遇,指向同一个节点,当指向同一个节点时,遍历结束。
代码实现
function ListNode(val){
this.val = val
this.next = null
}
const hasCycle = head => {
if(!head) return false
let pre = head, cur = head
while(cur && cur.next){
pre = pre.next
cur = cur.next.next
if(pre === cur){
return true
}
}
return false
}
环形链表2
const detectCycle = (head) => {
if(!head) return null
let pre = head, cur = head
while(cur && cur.next){
pre = pre.next
cur = cur.next.next
if(pre === cur){
let temp = head
while(pre !== temp){
pre = pre.next
temp = temp.next
}
return pre
}
}
return null
}
反转链表
思路
定义指针pre、cur、next, pre指向null,cur指向我们的头节点,next指向cur所指向节点的下一个节点。
指针初始化完毕,然后重复上述操作,当cur指针指向null的时候,我们就完成了整个链表的反转
代码实现
const reverseList = head => {
if(!head) return null
let pre = null, cur = head
while(cur){
[cur.next, pre, cur] = [pre, cur, cur.next]
}
return pre
}
反转链表2
思路
需要将第m个节点到第n个节点的链表进行反转。例如m=2,n=4
定义一个虚拟头节点,命名叫做hair,将它指向我们的真实头节点。
定义一个指针pre指向虚拟头节点。
定义一个指针cur指向pre指针所指向节点的下一个节点。
pre指针和cur指针同时向后移动,直到找到第m个节点。
定义指针con和tail,con指向pre所指向的节点,tail指向cur指针所指向的节点。
con所指向的节点,将是我们将部分链表反转后,部分链表头节点的前驱节点。
tail则是部分链表反转后的尾节点。
开始链表反转,首先定义一个指针third指向cur所指向的节点的下一个节点。
然后将cur所指向的节点指向pre所指向的节点,将pre指针移动到cur指针所在的位置。
将cur指针移动到third指针所在的位置,直到我们的pre指针指向第n个节点,重复上面步骤。
此时pre指针指向了第m个节点并且将第m到第n个节点之间反转完毕。
将con指针所指向的节点指向pre指针指向的节点。
将tail指针所指的节点指向cur指针所指的节点。
显示最终链表。
代码实现
const reverseBetween = (head, left, right) =>{
if(!head) return null
let ret = new ListNode(-1, head), pre =ret, cnt = right - left + 1
while(--left){
pre = pre.next
}
pre.next = reverse(pre.next, cnt)
return ret.next
}
const reverse = (head,n) =>{
let pre = null, cur = head
while(n--){
[cur.next, pre, cur] = [pre, cur, cur.next]
}
head.next = cur
return pre
}
K个一组反转链表
const reverseKGroup = (head, k) => {
if(!head) return null
let ret = new ListNode(-1, head), pre = ret
do{
pre.next = reverse(pre.next, k)
for(let i=0;i< k && pre; i++){
pre = pre.next
}
if(!pre) break;
}while(1);
return ret.next
}
const reverse = (head,n) => {
let pre = head, cur = head, con = n
while(--n && pre){
pre = pre.next
}
if(!pre) return head
pre = null
while(con--){
[cur.next, pre, cur] = [pre, cur, cur.next]
}
head.next = cur
return pre
}
快乐数
思路
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为1,也可能是无限循环但始终变不到1。
- 如果可以变为1,那么这个数就是快乐数。
- 如果n是快乐数就返回 true,否则返回 false。
代码实现
const isHappy = n => {
let pre = n, cur = getNext(n)
while(cur !== pre && cur !==1){
pre = getNext(pre)
cur = getNext(getNext(cur))
}
return cur === 1
}
const getNext = (n) => {
let t = 0
while(n){
t+=(n%10) * (n%10)
n = Math.floor(n/10)
}
return t
}
删除链表的倒数第N个结点
思路
- 通过双指针找到倒数第n个节点 back,back 的前一个节点 prev。
- 将 prev 的下个节点指向 back 的下个节点。
- 如果 prev 为空则代表删除头节点,返回 head.next 即可。
- 如果 prev 不为空时返回head。
代码实现
const removeN = (head, n) => {
let front = back = head
let prev = null
// 指针从头走 n-1 步找到第 n 个节点
while (--n) {
front = front.next
}
// 双指针同时走,前面指针到达链表位节点时,后指针为倒数第 n 个节点,记录第 n 个节点的前一个节点 prev
while (front.next) {
front = front.next
prev = back
back = back.next
}
//如果 prev 为空则表示删除头节点
if (!prev) return head.next
//删除 back 节点,即将 back 的前节点指向 back 的后一个节点
prev.next = back.next
return head
}
删除排序链表中的重复元素
思路
遍历链表,如果发现当前元素和下一个元素值相同,则删除下一个元素值。
代码实现
const deleteDuplicates = (head) => {
let p = head
while (p && p.next) {
if (p.val === p.next.val) {
p.next = p.next.next
} else {
p = p.next
}
}
return head
}
删除排序链表中的重复元素2
思路
-
链表已经过升序排列,所以只需要比较当前节点和下一个节点的值的大小,但是这道题比较难一点,新增变量
isDup记录是否删除当前节点。 -
可能头指针需要删除,为了避免每次循环进行判断,在头指针前面插入一个节点
result。 -
待会儿要返回整个链表,所以找个需要找个代跑
prev,以保留result头指针的位置。 -
每次只需要比较当前节点
prev.next和下个节点prev.next.next是否相同即可。 -
如果当前节点与下一个节点的值相同,删除下一个元素,同时标记一下
isDup = true, 先继续匹配还有没相同的值, 如果没有则下一轮删除掉当前节点。
代码实现
const deleteDuplicates = (head) => {
const result = new ListNode(0, head)
let isDup = false, prev = result
// prev.next指当前节点, prev.next.next指下个节点
while(prev.next && prev.next.next) {
if (prev.next.val === prev.next.next.val) {
// 标记,待会儿删除当前节点
isDup = true
// 删除下个节点
prev.next.next = prev.next.next.next
} else {
// 判断是否需要删除当前节点
if (isDup) {
prev.next = prev.next.next
isDup = false
} else {
prev = prev.next
}
}
}
// 循环结束, 判断是否有需要删除当前节点
if (isDup) {
prev.next = null
}
return result.next
}