数据结构与算法学习--链表

140 阅读4分钟

链表结构非常多的,今天学习下三种常见的链表:单链表、双向链表和循环链表。

单链表

链表通过指针将一组零散的内存块串联在一起。其中,内存块被称为链表的结点。为了将所有的结点串起来, 每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址 ,这个地址被称为后继指针next。结点中有两个结点比较特殊的,第一个结点被称为头结点,用来记录链表的基地址;最后一个结点被称为尾结点,尾结点是一个空地址NULL,表示这是链表上的最后一个结点。

用JavaScript实现一个单向链表,并且实现增删查:

首先定义一个类,构造函数中定义结点以及next:

class List{
	constructor(val){
		this.val = val;
    	this.next = null;
	}
}

先写一个查询方法,查询结点是否在该链表中:

find(target){
	let cur = this;
	console.log(111,this)
    while (cur.val !== target) {
        cur = cur.next;
        if (!cur) {
            return false
        }
    }
    return cur
}

再来一个添加方法

add(node, target){
	let newNode = new List(node);
    let cur = this.find(target);
    newNode.next = cur.next;
    cur.next = newNode
}

结点是由一个个的后继指针next串联起来的,添加方法就需要两个参数,一个待添加结点,一个添加的目标节点。在方法中首先将待添加的结点实例化为我们的结点对象,再将目标节点的next指针指向带添加结点。

测试一下:

let list = new List('nick')
list.add('tom', 'nick')

console.log(list)
//List {val: 'nick', next: List}

再来一个删除结点的方法,要删除某一个结点,根据链表的结构,直接将该链表的上一个结点的next指向目标结点的next,该结点就从整个链表中删除了。

findPre(target){
	if(!this.find(target)){
		return false;
	} 
	let cur = this;
    while (cur.next.val !== target) {
        cur = cur.next
    }
    return cur;
}

delete(target){
	let deleteNode = this.find(target);
    this.findPre(deleteNode.val).next = deleteNode.next
}

测试一下

let list = new List('老大')
list.add('老二', '老大')
list.add('老三', '老二')
list.add('老四', '老三')
list.delete('老三')

console.log(list)
//next: List
    next: List
    next: null
    val: "老四"
    [[Prototype]]: Object
    val: "老二"
    [[Prototype]]: Object
    val: "老大"
    [[Prototype]]: Object

完整代码:

class List{
	constructor(val){
		this.val = val;
    	this.next = null;
	}
	find(target){
		let cur = this;
        while (cur.val !== target) {
            cur = cur.next;
            if (!cur) {
                return false
            }
        }
        return cur
	}

	add(node, target){
		let newNode = new List(node);
        let cur = this.find(target);
        newNode.next = cur.next;
        cur.next = newNode
	}

	findPre(target){
		if(!this.find(target)){
			return false;
		} 
		let cur = this;
        while (cur.next.val !== target) {
            cur = cur.next
        }
        return cur;
	}

	delete(target){
		let deleteNode = this.find(target);
        this.findPre(deleteNode.val).next = deleteNode.next
	}
}
let list = new List('老大')
list.add('老二', '老大')
list.add('老三', '老二')
list.add('老四', '老三')
list.delete('老三')

console.log(list)
循环链表

就像图中展示的那样,单链表的尾结点指向头部结点,即是一个循环链表。

双向链表

单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。而双向链表,顾名思义,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。

双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。

用JavaScript实现一个单向链表,并且实现增删查:

class List{
	constructor(val){
		this.val = val;
    	this.next = null;
    	this.pre = null;
	}
	find(target){
		let cur = this;
        while (cur.val !== target) {
            cur = cur.next;
            if (!cur) {
                return false
            }
        }
        return cur
	}

	add(node, target){
		let newNode = new List(node);
        let cur = this.find(target);
        newNode.next = cur.next;
        newNode.pre = cur;
        cur.next = newNode

	}

	delete(target){
		let cur = this.find(target);
        cur.pre.next = cur.next;
        cur.next.pre = cur.pre;
	}
}
let list = new List('老大')
list.add('老二', '老大')
list.add('老三', '老二')
list.add('老四', '老三')
// list.delete('老三')

console.log(list)

有了单向链表的经验,双向链表就容易很多了,因为有了前驱指针 prev使得双向链表在增加删除时比单向链表方便太多。

反转链表
var reverseList = function(head) {
  let prev = null;
  let curr = head;
  while (curr) {
      const next = curr.next; //1
      curr.next = prev; //2
      prev = curr; //3
      curr = next; //4
  }
  return prev;
};

这里是力扣的官方答案,代码很简洁,有点难以理解。核心代码就是循环体内的四行代码,第一行和第四行只是为了循环体走下去,主要就是第二行和第三行。第一次循环,因为prev初始化时为null,此时第一次循环结束,prev就是第一个链表结点;第二次循环 ,此时的curr就是链表的第二个结点至结束的链表,将他的next指向prev,也就是第一个结点,此时的curr就是第二个结点指向第一个结点的链表;接下来依次循环,改链表就被反转了。不得不叹服,真的牛,这个方法,相想出这个方法的人而是真的聪明。

还有一个递归的方法

var reverseList = function(head) {
    if (head == null || head.next == null) {
        return head;
    }
    const newHead = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return newHead;
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/reverse-linked-list/solution/fan-zhuan-lian-biao-by-leetcode-solution-d1k2/
来源:力扣(LeetCode

这里也太难理解了,有个视频讲解的很好,以1->2->3->4为例,首先将1和2->3->4进行反转,就是head.next.next = head,head.next = null,随后递归反转。