链表
链表不需要一块连续的内存空间来存储,它通过“指针”将一组零散的内存块串联起来使用
三种最常见的链表结构:单链表、双向链表和循环链表
单链表
为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址
从图中我们可以看出,针对链表的插入和删除操作,我们只需要考虑相邻结点的指针改变,所以对应的时间复杂度是 O(1)
链表要想随机访问第 k 个元素,就没有数组那么高效了。因为链表中的数据并非连续存储的,所以无法像数组那样,根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个结点一个结点地依次遍历,直到找到相应的结点。链表随机访问的性能没有数组好,需要 O(n) 的时间复杂度。
设计一个基于对象的单链表
//Node 类用来表示节点
function Node(element) {
this.element = element;
this.next = null;
}
// 链表类
function LinkedList() {
this.head = new Node("head");
this.find = find;
this.insert = insert;
this.remove = remove;
this.display = display;
this.findPrevious = findPrevious;
this.remove = remove;
}
//该方法遍历链表,查找给定数据
function find(item) {
var currNode = this.head;
while (currNode.element != item) {
currNode = currNode.next;
}
return currNode;
}
//将新节点插入链表
function insert(newElement, item) {
var newNode = new Node(newElement);
var current = this.find(item);
newNode.next = current.next;
current.next = newNode;
}
// 输出当前链表
function display() {
var currNode = this.head;
while (!(currNode.next == null)) {
console.log(currNode.next.element);
currNode = currNode.next;
}
}
// 查找待删除节点前面的节点
function findPrevious(item) {
var currNode = this.head;
while (!(currNode.next == null) && (currNode.next.element != item)) {
currNode = currNode.next;
}
return currNode;
}
// 删除节点
function remove(item) {
var prevNode = this.findPrevious(item);
if (!(prevNode.next == null)) {
prevNode.next = prevNode.next.next;
}
}
var cities = new LinkedList();
cities.insert("Conway", "head");
cities.insert("Russellville", "Conway");
cities.insert("Carlisle", "Russellville");
cities.insert("Alma", "Carlisle");
cities.display();
cities.remove("Carlisle");
cities.display();
循环链表
循环链表的尾结点指针是指向链表的头结点
双向链表
双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历
链表 VS 数组性能大比拼
和数组相比,链表更适合插入、删除操作频繁的场景,查询的时间复杂度较高。
将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。
if (head == null) { head = new_node;} //插入头结点
if (head->next == null) { head = null;} // 插入尾节点
重点留意边界条件处理
我经常用来检查链表代码是否正确的边界条件有这样几个:
- 如果链表为空时,代码是否能正常工作?
2.如果链表只包含一个结点时,代码是否能正常工作?
3.如果链表只包含两个结点时,代码是否能正常工作?
4.代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
5个常见的链表操作
- 单链表反转
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
if(head == null || head.next == null){
return head
}
const current = reverseList(head.next);
//例如,1,2,3,4,5,null
//current是5
//head是4
//head.next 是 5
//head.next.next 就是5指向的指针,指向当前的head(4)
//5-4-3-2-1-null
head.next.next = head;
//注意把head.next设置为null,切断4链接5的指针
head.next = null
//每层递归返回当前的节点,也就是最后一个节点。(因为head.next.next改变了,所以下一层current变4,head变3)
return current;
};
- 链表中环的检测
- 两个有序的链表合并删除
- 链表倒数第 n 个结点
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
解题思路:用两个指针,让第一个先走k步,然后两个指针一起移动,当第一个指针到最后一个节点处,第二个指针就在倒数第K个节点
var getKthFromEnd = function(head, k) {
let first = head,
second = head;
while (k !== 0) {
first = first.next;
k--;
}
while (first !== null) {
first = first.next;
second = second.next;
}
return second;
};
- 求链表的中间结点
如果您对本文有什么疑问,欢迎提出个人见解,若您觉得本文对你有用,不妨帮忙点个赞,或者在评论里给我一句赞美,小小成就都是今后继续为大家编写优质文章的动力, 欢迎您持续关注我的博客:)
作者:Jesse131
出处:http://www.cnblogs.com/jesse131/
关于作者:专注前端开发。如有问题或建议,请多多赐教!
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。