数据结构之----链表

256 阅读8分钟

链表的定义

链表是物理存储单元上非连续的、非顺序的存储结构,由一系列节点组成,下图是一个简单的结构示意图,本节课程所提到的链表,均指单链表。

节点

节点包含两部分,一部分是存储数据元素的数据域,一部分是存储指向下一个节点的指针域,上图中绿色的部分就是数据域,蓝色的部分是指针域,他们一起共同构成一个节点。一个节点可以用如下的方式去定义和使用,示例代码如下:

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));
递归翻转链表思路分析

递归的思想,精髓之处在于甩锅,你做不到的事情,让别人去做,等别人做完了,你在别人的基础上继续做。

甩锅一共分为四步:

  1. 明确函数的功能,既然是先让别人去做,那你得清楚的告诉他做什么。函数reverse_digui(head)完成的功能,是从head开始翻转链表,函数返回值是翻转后的头节点

  2. 正式甩锅,进行递归调用,就翻转链表而言,甩锅的方法如下

  3.  var new_head = reverse_digui(head.next);
    
  4. 原本是翻转以head开头的链表,可是你不会啊,那就先让别人从head.next开始翻转链表,等他翻转完,得到的new_head就是翻转后的头节点。

  5. 根据别人的结果,计算自己的结果
    第二步中,已经完成了从head.next开始翻转链表,现在,只需要把head连接到新链表上就可以了,新链表的尾节点是head.next,执行head.next.next = head,这样,head就成了新链表的尾节点。
    根据上面的三步,代码不难写出来

  6.  // 递归翻转
     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这三个方法,其他的方法如果你能力足够强,也可以一并实现。