链表的定义
链表是物理存储单元上非连续的、非顺序的存储结构,由一系列节点组成,下图是一个简单的结构示意图,本节课程所提到的链表,均指单链表。
节点
节点包含两部分,一部分是存储数据元素的数据域,一部分是存储指向下一个节点的指针域,上图中绿色的部分就是数据域,蓝色的部分是指针域,他们一起共同构成一个节点。一个节点可以用如下的方式去定义和使用,示例代码如下:
var Node = function(data){
this.data = data;
this.next = null;
}
var node1 = new Node(1);
var node2 = new Node(2);
var node3 = new Node(3);
node1.next = node2;
node2.next = node3
console.log(node1.data);
console.log(node1.next.data);
console.log(node1.next.next.data);
首尾节点
链表中的第一个节点是首节点,最后一个节点是尾节点。
有头链表和无头链表
无头链表是指第一个节点既有数据域,又有指针域,第一个节点既是首节点又是头节点。
有头链表是指第一个节点只有指针域,而没有数据域。
在链表定义中展示的就是无头链表,一个有头链表的结构图如下
链表的方法
链表的方法如下:
-
append, 添加一个新的元素
-
insert,在指定位置插入一个元素
-
remove,删除指定位置的节点
-
remove_head,删除首节点
-
remove_tail,删除尾节点
-
indexOf,返回指定元素的索引
-
get,返回指定索引位置的元素
-
head,返回首节点
-
tail,返回尾节点
-
length,返回链表长度
-
isEmpty,判断链表是否为空
-
clear,清空链表
-
print,打印整个链表
function LinkList(){ // 定义节点 var Node = function(data){ this.data = data; this.next = null; }
var length = 0; // 长度 var head = null; // 头节点 var tail = null; // 尾节点 // 添加一个新元素 this.append = function(data){ // 创建新节点 var node = new Node(data); // 如果是空链表 if(head==null){ head = node; tail = head; }else{ tail.next = node; // 尾节点指向新创建的节点 tail = node; // tail指向链表的最后一个节点 } length += 1; // 长度加1 return true; }; // 返回链表大小 this.length = function(){ return length; }; // 获得指定位置的节点 var get_node = function(index){ if(index < 0 || index >= length){ return null; } var curr_node = head; var node_index = index; while(node_index-- > 0){ curr_node = curr_node.next; } return curr_node; }; // 在指定位置插入新的元素 this.insert = function(index, data){ // index == length,说明是在尾节点的后面新增,直接调用append方法即可 if(index == length){ return this.append(data); }else if(index > length || index < 0){ // index范围错误 return false; }else{ var new_node = new Node(data); if(index == 0){ // 如果在头节点前面插入,新的节点就变成了头节点 new_node.next= head; head = new_node; }else{ // 要插入的位置是index,找到索引为index-1的节点,然后进行连接 var pre_node = get_node(index-1); new_node.next = pre_node.next; pre_node.next = new_node; } length += 1; return true; } }; // 删除指定位置的节点 this.remove = function(index){ // 参数不合法 if(index < 0 || index >= length){ return null; }else{ var del_node = null; // 删除的是头节点 if(index == 0){ // head指向下一个节点 del_node = head; head = head.next; // 如果head == null,说明之前链表只有一个节点 if(!head){ tail = null; } }else{ // 找到索引为index-1的节点 var pre_node = get_node(index-1); del_node = pre_node.next; pre_node.next = pre_node.next.next; // 如果删除的是尾节点 if(del_node.next==null){ tail = pre_node; } } length -= 1; del_node.next = null; return del_node.data; } }; // 删除尾节点 this.remove_tail = function(){ return this.remove(length-1); }; // 删除头节点 this.remove_head = function(){ return this.remove(0); }; // 返回指定位置节点的值 this.get = function(index){ var node = get_node(index); if(node){ return node.data; } return null; }; // 返回链表头节点的值 this.head = function(){ return this.get(0); } // 返回链表尾节点的值 this.tail = function(){ return this.get(length-1); } // 返回指定元素的索引,如果没有,返回-1 // 有多个相同元素,返回第一个 this.indexOf = function(data){ var index = -1; var curr_node = head; while(curr_node){ index += 1 if(curr_node.data == data){ return index; }else{ curr_node = curr_node.next; } } return -1; }; // 输出链表 this.print = function(){ var curr_node = head; var str_link = "" while(curr_node){ str_link += curr_node.data.toString() + " ->"; curr_node = curr_node.next; } str_link += "null"; console.log(str_link); console.log("长度为"+ length.toString()); }; // isEmpty this.isEmpty = function(){ return length == 0; }; // 清空链表 this.clear = function(){ head = null; tail = null; length = 0; };};
基于链表实现的Stack 和 Queue
之前所学的Stack和Queue都是基于数组实现的,有了链表以后,则可以基于链表实现。
基于链表实现的栈:
unction Stack(){
var linklist = new LinkList();
// 从栈顶添加元素
this.push = function(item){
linklist.append(item);
};
// 弹出栈顶元素
this.pop = function(){
return linklist.remove_tail();
};
// 返回栈顶元素
this.top = function(){
return linklist.tail();
};
// 返回栈的大小
this.size = function(){
return linklist.length();
};
// 判断是否为空
this.isEmpty = function(){
return linklist.isEmpty();
};
// 清空栈
this.clear = function(){
linklist.clear()
};
};
基于链表实现的队列:
function Queue(){
var linklist = new LinkList();
// 入队列
this.enqueue = function(item){
linklist.append(item);
};
// 出队列
this.dequeue = function(){
return linklist.remove_head();
};
// 返回队首
this.head = function(){
return linklist.head();
};
// 返回队尾
this.tail = function(){
return linklist.tail();
};
// size
this.size = function(){
return linklist.length();
};
//clear
this.clear = function(){
linklist.clear();
};
// isEmpty
this.isEmpty = function(){
return linklist.isEmpty();
};
};
翻转链表
使用迭代和递归两种方法翻转链表,下面的代码已经准备好了上下文环境,请实现函数reverse_iter和reverse_digui。
在考虑算法时,多数情况下你考虑边界情况会让问题变得简单,但边界情况往往不具备普适性,因此,也要尝试考虑中间的情况,假设链表中间的某个点为curr_node,它的前一个节点是pre_node,后一个节点是next_node,现在把思路聚焦到这个curr_node节点上,只考虑在这一个点上进行翻转,翻转方法如下:
curr_node.next = pre_node;
只需要这简单的一个步骤就可以完成对curr_node节点的翻转,对于头节点来说,它没有上一个节点,让 pre_node=null,表示它的上一个节点是一个空节点。
在遍历的过程中,每完成一个节点的翻转,都让curr_node = next_node,找到下一个需要翻转的节点。同时,pre_node和next_node也跟随curr_node一起向后滑动。
示例代码:
// 迭代翻转
// 迭代翻转
function reverse_iter(head){
if(!head){
return null;
}
var pre_node = null; // 前一个节点
var curr_node = head; // 当前要翻转的节点
while(curr_node){
var next_node = curr_node.next; // 下一个节点
curr_node.next = pre_node; // 对当前节点进行翻转
pre_node = curr_node; // pre_node向后滑动
curr_node = next_node; // curr_node向后滑动
}
//最后要返回pre_node,当循环结束时,pre_node指向翻转前链表的最后一个节点
return pre_node;
};
print(reverse_iter(node1));
递归翻转链表思路分析
递归的思想,精髓之处在于甩锅,你做不到的事情,让别人去做,等别人做完了,你在别人的基础上继续做。
甩锅一共分为四步:
-
明确函数的功能,既然是先让别人去做,那你得清楚的告诉他做什么。函数reverse_digui(head)完成的功能,是从head开始翻转链表,函数返回值是翻转后的头节点
-
正式甩锅,进行递归调用,就翻转链表而言,甩锅的方法如下
-
var new_head = reverse_digui(head.next); -
原本是翻转以head开头的链表,可是你不会啊,那就先让别人从head.next开始翻转链表,等他翻转完,得到的new_head就是翻转后的头节点。
-
根据别人的结果,计算自己的结果
第二步中,已经完成了从head.next开始翻转链表,现在,只需要把head连接到新链表上就可以了,新链表的尾节点是head.next,执行head.next.next = head,这样,head就成了新链表的尾节点。
根据上面的三步,代码不难写出来 -
// 递归翻转 function reverse_digui(head){ // 如果head 为null if(!head){ return null; } if(head.next==null){ return head; } // 从下一个节点开始进行翻转 var new_head = reverse_digui(head.next); head.next.next = head; // 把当前节点连接到新链表上 head.next = null; return new_head; };
从尾到头打印链表
当你拿到一个链表时,得到的是头节点,只有头结点以后的节点被打印了,才能打印头节点,这不正是一个可以甩锅的事情么,先定义函数
var Node = function(data){
this.data = data;
this.next = null;
}
var node1 = new Node(1);
var node2 = new Node(2);
var node3 = new Node(3);
var node4 = new Node(4);
var node5 = new Node(5);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
function reverse_print(head){
// 递归终止条件
if(head==null){
return
}else{
reverse_print(head.next); // 甩锅
console.log(head.data); // 后面的都打印了,该打印自己了
}
};
合并两个两个有序链表
已知有两个有序链表(链表元素从小到大),请实现函数merge_link,将两个链表合并成一个有序链表,并返回新链表,原有的两个链表不要修改。
合并两个有序链表,是归并排序在链表上的一种实践。对两个链表,各自设置一个游标节点指向头节点,对游标节点上的数值进行比较,数值小的那个拿出来放入到合并链表中,同时游标节点向后滑动,继续比较游标节点数值大小。
为了实现滑动,需要使用一个while循环,当其中一个游标节点为null时,循环终止,这时,可能另一个游标节点还没有到达尾节点,那么把这段还没有遍历结束的链表添加到合并列表上。
function merge_link(head1, head2){
if(head1 == null){
return head2;
}else if(head2 == null){
return head1;
}
var merge_head = null; // 合并后链表头
var merge_tail = null; // 合并后链表尾
var curr_1 = head1;
var curr_2 = head2;
while(curr_1 && curr_2){
// 找到最小值
var min_data;
if(curr_1.data < curr_2.data) {
min_data = curr_1.data;
curr_1 = curr_1.next;
}else{
min_data = curr_2.data;
curr_2 = curr_2.next;
}
if(merge_head == null){
merge_head = new Node(min_data);
merge_tail = merge_head;
}else{
var new_node = new Node(min_data);
// 把new_node连接到合并链表
merge_tail.next = new_node;
// 尾节点指向新创建的节点
merge_tail = new_node;
}
}
// 链表可能还有一部分没有合并进来
var rest_link = null;
if(curr_1){
rest_link = curr_1;
}
if(curr_2){
rest_link = curr_2;
}
while(rest_link){
var new_node = new Node(rest_link.data);
merge_tail.next = new_node;
merge_tail = new_node;
rest_link = rest_link.next;
}
return merge_head;
};
查找单链表中的倒数第K个节点(k > 0)
var Node = function(data){
this.data = data;
this.next = null;
}
var node1 = new Node(1);
var node2 = new Node(2);
var node3 = new Node(3);
var node4 = new Node(4);
var node5 = new Node(5);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
function reverse_find(head, k){
// 在这里实现你的代码,返回倒数第k个节点的值
var fast = head;
var slow = head;
var step = k;
// 先让快游标的先走k步
while(step > 0 && fast){
fast = fast.next;
step -= 1;
}
// 当循环结束时,如果step != 0,说明链表的长度不够k
if(step!=0){
return null;
}else{
// 快的和慢的游标一起走
while(fast && slow){
fast = fast.next;
slow = slow.next;
}
}
return slow.data;
};
console.log(reverse_find(node1, 2));
查找单链表的中间结点
var Node = function(data){
this.data = data;
this.next = null;
};
var node1 = new Node(1);
var node2 = new Node(2);
var node3 = new Node(3);
var node4 = new Node(4);
var node5 = new Node(5);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
function find_middle(head){
// 在这里实现你的代码,返回倒数第k个节点的值
var fast = head;
var slow = head;
// 两个一起走,fast一次走两步,slow一次走一步
while(fast.next){
slow = slow.next;
fast = fast.next.next;
}
return slow.data;
};
console.log(find_middle(node1));
实现双向链表
和单链表不同的是,双向链表的每个节点多出来一个pre指针域,指向它的前驱节点。
下面是双向链表的定义,请实现append,insert,remove这三个方法,其他的方法如果你能力足够强,也可以一并实现。