一. 数组和链表的区别
- 数组使用的是一组
连续的内存空间
来存储数据,支持随机访问数组中的元素;
内存寻址公式:a[i]_address = base_address + i * data_type_size
数组元素的下标 i 其实理解为元素相对于数组基地址的
“偏移量 offset ”
更合适。
- 数据为了保证数据使用内存的连续性,在插入和删除元素时,需要搬移大量数据,效率较低;
- 链表是将
零散的内存串联起来
存储数据的,插入和删除元素的时间复杂度为 O(1),优于数组; - 链表节点内存上不具有连续性,不支持快速地随机访问数据中的元素;
- 数组使用的是连续的内存空间,便于 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 缓存
思路:
使用 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 为缓存数量)