链表和数组的区别
参考 在聊这个问题之前,先看一下数据从逻辑结构上的分类。主要分为两类:线性表和非线性表。
线性表: 数据连成一条线的结构,今天要聊的链表和数组就属于这一类,除此之外还有栈,队列等。
非线性表: 数据之间的关系是非线性的,如树,堆,图等。
看完线性表和非线性表之后,来继续看下:
数组的定义:数据是一种线性表数据结构,它用一组连续的内存空间,来存储一组具有相同类型的数据。
由于数组在内存中是连续存放的,所以通过下标来随机访问数组中的元素效率是非常高的。但与此同时,为了保证连续性,如果想在数组中添加一个元素,需要大量地对数据进行搬移工作。同理想在数组中删除一个元素也是如此。所以我们得出一个结论:在数组中随机访问的效率很高,但是执行添加和删除时效率低下,平均时间复杂度为O(n)。
链表的定义: 是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
刚才介绍了在数组中添加和删除一个元素的效率是低下的。而链表的存储空间是不连续的,使用链表添加或者删除一个数据,我们并不需要为了保持内存的连续性而对数据进行搬移,所以在链表中添加和删除元素是非常高效的。但万事都有两面性,正因为链表的存储空间是不连续的,想要在链表中访问一个元素时,就无法像数组一样根据首地址和下标,通过寻址公式来计算出对应的节点。而只能通过指针去依次遍历找出相应的节点。所以我们得出一个结论:在链表中执行添加和删除操作时效率很高,而随机访问的效率很低,平均时间复杂度为O(n)。
单链表
创建单链表结构
// 声明链表节点
class Node {
constructor (value) {
this.val = value
this.next = null
}
}
// 声明链表的数据结构
class NodeList {
constructor (arr) {
// 声明链表的头部节点
let head = new Node(arr.shift())
let next = head
arr.forEach(item => {
next.next = new Node(item)
next = next.next
})
return head
}
}
let arr = [1,2,3]
let head = new NodeList(arr)
剑指从尾到头打印链表
一次遍历,数组反转
var reversePrint = function(head) {
let res = []
next = head
while(head) {
res.push(next.val)
if(next.next) {
next = next.next
}else {
return res.reverse()
}
}
return res
};
递归回溯,终止条件为null的时候,因为visitor为同步,当head为null时,visitor就会完整执行,执行结束之后返回上一个visitor执行的地方继续往下执行nums.push(),执行完毕在返回上一个visitor,这样就形成了回溯。
var reversePrint = function (head) {
let nums = []
const visitor = function (head) {
if (head !== null) {
visitor(head.next)
nums.push(head.val)
}
};
visitor(head)
return nums
}
利用栈先进后出的思想
var reversePrint = function(head) {
const stack = [],res = [];
let p = head,t = 0;
while(p){
stack.push(p.val);
t ++;
p = p.next;
}
for(let i = 0;i < t;i ++){
res.push(stack.pop());
}
return res;
}
链表中倒数第k个节点
参考 思路:在链表中,链表的长度是未知的,如果需要获得长度就需要遍历一遍链表,而且链表一般不能直接用,而是需要借助临时变量这样才可以放心的对链表进行操作。而题目中要求返回链表,实际上链表就是一个指针,将当前位置的指针返回就是链表结构。
var getKthFromEnd = function(head, k) {
let node = head, n = 0
while(node){
node = node.next
n++
}
node = head
for(let i = 0; i < n - k; i++) {
node = node.next
}
return node
};
法二:快慢针
思路:题目中求倒数第n个,也就意味着长度是n。定义两个指针,令两个指针的距离为n。当最前面的指针走到头,后面的指针就指向这个链表的开始。
var getKthFromEnd = function(head, k) {
let fast = head, low = head
while(fast && k > 0) {
fast = fast.next
k--
}
while(fast) {
fast = fast.next
low = low.next
}
return low
};
反转链表
法一:迭代
把链表的指针反转过来,指向上一个元素。
第一步:定义一个临时指针保存断开后后面的节点
第二步:让当前指针指向前驱节点
var reverseList = function(head) {
let pre = null, curr = head
while(curr) {
let tmpNext = curr.next
curr.next = pre
pre = curr
curr = tmpNext
}
return pre
}
法二:递归回溯
思路:首先利用递归,遍历到链表最末端,然后通过回溯赋值,将当前节点的next.next也就是下一个节点值指向指向自己,然后将自己的next指针指向null。
var reverseList = function(head) {
if (head == null || head.next == null) {
return head;
}
let newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
};
合并排序链表
思路:
合并两个排序的链表,可以借鉴归并排序的思路,两个链表的头指针进行比较,将小的一边放入合并的链表。
在实际操作过程中,需要新创建一个链表可以使用题目中的链表结构创建,然后再赋值时应该考虑的是链表的指针的指向,让next直接等于下一个节点
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var mergeTwoLists = function(l1, l2) {
let node = new ListNode(0)
let head = node
while(l1 && l2){
if(l1.val <= l2.val) {
node.next = l1
l1 = l1.next
}else if (l2.val < l1.val){
node.next = l2
l2 = l2.next
}
node = node.next
}
node.next = l1 ? l1 : l2
return head.next
};
删除链表重复值
思路:想要删除链表的或者改变链表的值,只能改变链表的下一个指针的指向。所以在当前结点就判断下一个结点是否重复,重复了就让p.next = p.next.next,使当前的next指向下下个结点。没有重复就p.next.val的值存起来。
/*function ListNode(x){
this.val = x;
this.next = null;
}*/
function deleteDuplication(pHead)
{
// write code here
let obj = {}
let p = pHead
obj[p.val] = 1
while(p.next) {
if(obj[p.next.val]) {
p.next = p.next.next
}else {
obj[p.next.val] = 1
p = p.next
}
}
return pHead
}
删除链表重复值2
/*function ListNode(x){
this.val = x;
this.next = null;
}*/
function deleteDuplication(pHead)
{
// write code here
class ListNode {
constructor(x, next) {
this.val = x
this.next = next
}
}
const dummy = new ListNode(0, pHead)
let cur = dummy
while(cur.next && cur.next.next) {
if(cur.next.val === cur.next.next.val) {
const tmp = cur.next.val
while(cur.next && cur.next.val === tmp) {
cur.next = cur.next.next
}
}else {
cur = cur.next
}
}
return dummy.next
}
module.exports = {
deleteDuplication : deleteDuplication
};
循环链表
参考
循环链表和单链表相似,节点类型都是一样,唯一的区别是,在创建循环链表的时候,让其头节点的 next 属性执行它本身,即
head.next = head;
这种行为会导致链表中每个节点的 next 属性都指向链表的头节点,换句话说,也就是链表的尾节点指向了头节点,形成了一个循环链表,如下图所示:
class ListNode {
constructor(x, head) {
this.val = x
this.next = head
}
}
class creatList {
constructor(arr) {
let head = new ListNode(arr.shift(), null)
let next = head
arr.forEach(item => {
next.next = new ListNode(item, head)
next = next.next
})
return head
}
}
let arr = [1,2,3]
let head = new creatList(arr)
for(let i = 0; i < 10; i++) {
console.log(head.val)
head = head.next
}
双向链表
//节点
function Node(element) {
this.element = element; //当前节点的元素
this.next = null; //下一个节点链接
this.previous = null; //上一个节点链接
}
//链表类
function LList () {
this.head = new Node( 'head' );
this.find = find;
this.findLast = findLast;
this.insert = insert;
this.remove = remove;
this.display = display;
this.dispReverse = dispReverse;
}
//查找元素
function find ( item ) {
var currNode = this.head;
while ( currNode.element != item ){
currNode = currNode.next;
}
return currNode;
}
//查找链表中的最后一个元素
function findLast () {
var currNode = this.head;
while ( !( currNode.next == null )){
currNode = currNode.next;
}
return currNode;
}
//插入节点
function insert ( newElement , item ) {
var newNode = new Node( newElement );
var currNode = this.find( item );
newNode.next = currNode.next;
newNode.previous = currNode;
currNode.next = newNode;
}
//显示链表元素
function display () {
var currNode = this.head;
while ( !(currNode.next == null) ){
console.debug( currNode.next.element );
currNode = currNode.next;
}
}
//反向显示链表元素
function dispReverse () {
var currNode = this.findLast();
while ( !( currNode.previous == null )){
console.log( currNode.element );
currNode = currNode.previous;
}
}
//删除节点
function remove ( item ) {
var currNode = this.find ( item );
if( !( currNode.next == null ) ){
currNode.previous.next = currNode.next;
currNode.next.previous = currNode.previous;
currNode.next = null;
currNode.previous = null;
}
}
var fruits = new LList();
fruits.insert('Apple' , 'head');
fruits.insert('Banana' , 'Apple');
fruits.insert('Pear' , 'Banana');
fruits.insert('Grape' , 'Pear');
console.log( fruits.display() ); // Apple
// Banana
// Pear
// Grape
console.log( fruits.dispReverse() ); // Grape
// Pear
// Banana
// Apple