数组和链表(回文链表、双向链表实现 LRU 缓存)

一. 数组和链表的区别

数组和链表的内存分配示意图

  1. 数组使用的是一组连续的内存空间来存储数据,支持随机访问数组中的元素;

内存寻址公式:a[i]_address = base_address + i * data_type_size

数组元素的下标 i 其实理解为元素相对于数组基地址的“偏移量 offset ”更合适。

  1. 数据为了保证数据使用内存的连续性,在插入和删除元素时,需要搬移大量数据,效率较低;
  2. 链表是将零散的内存串联起来存储数据的,插入和删除元素的时间复杂度为 O(1),优于数组;
  3. 链表节点内存上不具有连续性,不支持快速地随机访问数据中的元素;
  4. 数组使用的是连续的内存空间,便于 CPU 将数据预读到内存中,存为缓存,提高数据读取速度(缓存的存在,就是为了改善 CPU 读取数据的速度跟不上 CPU 处理速度的情形)。

二. 算法练习小例子

1. 回文链表的判断

回文链表
思路1:
遍历链表节点,将其中的值保存到数组中,使用数组的 reverse() 和 join() 方法,就能比较轻松的判断出是否回文串。

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @return {boolean}
 */
var isPalindrome = function (head) {
    let arr = [];
    let currentNode = head;
    while (currentNode) {
        arr.push(currentNode.val);
        currentNode = currentNode.next;
    }
    let reverseArr = [...arr].reverse();
    return arr.join('') === reverseArr.join('');
};

时间复杂度:O(n)
空间复杂度:O(n)

思路2:
基本步骤放在代码注释中了。

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

// 反转列表
function reverseLinkedList(head) {
    let prevNode = null;
    let currentNode = head;
    while (currentNode) {
        let nextNode = currentNode.next;
        currentNode.next = prevNode;
        prevNode = currentNode;
        currentNode = nextNode;
    }
    return prevNode;
}

// 找到链表的中间节点
function getCenterNode(head) {
    let slowP = head;
    let fastP = head;
    while (fastP.next !== null && fastP.next.next !== null) {
        slowP = slowP.next;
        fastP = fastP.next.next;
    }
    console.log(slowP);
    return slowP;
}

/** 
 * 判断是否回文链表
 * @param {ListNode} head
 * @return {boolean}
 */
var isPalindrome = function (head) {
    let result = true;
    if (!head || !head.next) {
        return result;
    }
    let firstHead = head;
    // 1. 找到链表的中间节点
    let centerNode = getCenterNode(firstHead);
    // 2. 反转链表中间节点后的链表节点
    let secondHead = reverseLinkedList(centerNode.next);
    // 3. 判断前半段链表与反转后的后半段链表是否相同,相同则为回文链表
    console.log(secondHead);
    while (firstHead && secondHead) {
        if (firstHead.val !== secondHead.val) {
            result = false;
            break;
        }
        firstHead = firstHead.next;
        secondHead = secondHead.next;
    }
    // 4. 还原之前反转的后半段链表(反转 2 次就是还原)
    centerNode.next = reverseLinkedList(centerNode.next);
    // 5. 返回结果
    return result;
};

时间复杂度:O(n)
空间复杂度:O(1)

2. 链表实现 LRU 缓存

LRU缓存
输入示例
思路:

使用 map 来存储键值对的对应关系,key 为键值,value 为缓存链表中的节点;
使用双向链表来实现缓存列表,构建这样具有这些特性的链表:越靠近头部的节点,就是最近使用过的;最后的节点是最近没有使用的。

tip:
给链表添加辅助的头结点和尾节点,可以省去节点操作中的很多判断逻辑!

// 双向链表节点构造函数
function DLinkedNode(key, value) {
    this.key = key;
    this.value = value;
    this.next = null;
    this.previous = null;
}

/**
 * @param {number} capacity
 */
var LRUCache = function (capacity) {
    // 缓存最大尺寸
    this.capacity = capacity;
    // 缓存中实际存储的节点数量
    this.size = 0;

    // map:用于存储key以及对应的节点
    this.cache = new Map();
    // 添加了辅助头节点和辅助尾节点的双向链表
    this.head = new DLinkedNode();
    this.tail = new DLinkedNode();
    this.head.next = this.tail;
    this.tail.previous = this.head;
};

/** 
 * 将节点移动到链表头部
 * @param {node} DLinkedNode
 * @return {void}
 */
LRUCache.prototype.moveToHead = function (node) {
    node.next.previous = node.previous;
    node.previous.next = node.next;
    this.addToHead(node);
};

/** 
 * 将链表尾部的节点移除
 * @return {node} DLindkedNode
 */
LRUCache.prototype.removeTailNode = function () {
    let node = this.tail.previous;
    this.tail.previous = this.tail.previous.previous;
    this.tail.previous.next = this.tail;
    return node;
};

/**
 * 将节点插入到链表头部
 * @param {node} DLinkedNode
 * @return {void}
 */
LRUCache.prototype.addToHead = function (node) {
   this.head.next.previous = node;
   node.previous = this.head;
   node.next = this.head.next;
   this.head.next = node;
};

/** 
 * 1. 缓存中key存在,将对应节点移动到链表头部,返回节点的value
 * 2. 缓存中key不存在,返回-1
 * @param {number} key
 * @return {number} value
 */
LRUCache.prototype.get = function (key) {
    let node = this.cache.get(key);
    if (!node) {
        return -1;
    } else {
        this.moveToHead(node);
        return node.value;
    }
};

/** 
 * 1. key 值对应的节点不存在,就使用传进的 key 和 value 来构建一个新的节点,并插入到头部,更新 map
 * 2. key 值对应的节点存在,就更新已有节点的 value,并把它移到头部, 更新 map
 * @param {number} key 
 * @param {number} value
 * @return {void}
 */
LRUCache.prototype.put = function (key, value) {
    let node = this.cache.get(key);
    if (!node) {
        let newNode = new DLinkedNode(key, value);
        this.addToHead(newNode);
        this.cache.set(key, newNode);
        this.size++;
        if (this.size > this.capacity) {
            let removedNode = this.removeTailNode();
            this.cache.delete(removedNode.key);
            this.size--;
        }
    } else {
        node.value = value;
        this.cache.set(node.key, node);
        this.moveToHead(node);
    }
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * var obj = new LRUCache(capacity)
 * var param_1 = obj.get(key)
 * obj.put(key,value)
 */

时间复杂度:O(1)
空间复杂度:O(capacity) (capacity 为缓存数量)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值