数据结构之--链表(java)

引言

前文讲过的数组是线性表的一种表现形式,另外一种形式是链表。链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点组成,每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域(可能还有其他指针,比如双向链表中指向父节点的指针)。

在java中链表的的每个节点都是一个类对象,单向链表的数据结构一般为:
public class Node {
    int element;//数据域
    Node next;//指向下一个节点的指针


    public Node(int element) {
        this.element = element;
    }
}

链表的特征

链表一般分为:单向链表,单向循环链表、双向链表、双向循环链表。还有带头结点的链表,空链表的。

单向链表:有两个特殊的节点,首元结点:没有前驱的节点;尾节点:没有后继的节点(next指针指向null)。有的单向链表还带有头节点,所谓头节点 只是为了操作方便而创建的节点,一般不存放数据(且不记录到链表总长度重),next指针永远指向头节点。如果头节点的next指针指向null,说明是空链表。单向链表智能从头到尾依次遍历进行查找。

单向循环链表:与单向链表的唯一区别就是,单向循环链表的尾节点的next指针指向的是首元节点,此时链表中的所有元素都有一个前驱和后继,构成一个环状。

双向链表和双向循环链表,只是在单向的基础上添加了一个pre指针指向前驱节点。双向链表不仅可以从头到尾进行遍历,还可以从尾到头进行遍历,但每个节点多维护了一个指针,空间消耗增加。比如java api中的LinkedList,就是一个双向链表,在LinkedList中定义的节点 内部类为:
private static class Node<E> {
        E item;
        Node<E> next; //指向后继
        Node<E> prev;//指向前驱
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
如果没有特殊说明,一般说的链表指的就是单向链表。

链表中常见问题

1、在链表的某个指定节点前插入一个新节点。
最容易相当的办法,就是从头开始遍历找到这个指定节点的前一个节点,把前一个节点的next指针指向新节点,新节点的next指向指向指定的节点。 但如果指定节点刚好在链表的尾部,此时需要遍历整个链表(时间复杂度为O(N))。

更好的做法为:首先保存指定节点的next指针;再把新节点插入到指定节点后面(指定节点的next指针指向新节点,新节点的next指针再指向指定节点原来的next指针指向的节点)。此时新节点已经插入到指定节点后,但题目要求插入到指定节点前面,这时只需交换两个节点的数据域即可,具体实现如下(时间复杂度为O(1)):
/**
     * 指定节点前插入新节点
     * @param node 指定节点
     * @param in 待插入节点
     */
    public static void insert(Node node,Node in){
        Node temp =node.next;//保存指定节点的next指针
        node.next = in;//下一个节点指向 新插入节点
        in.next = temp;//新插入节点的next指针指向 指定节点原来的下一个节点

        //交换指定节点和新插入节点的数据域
        int tempe = in.element;
        in.element = node.element;
        node.element = tempe;
    }
2、反序打印链表
使用递归可以很简洁的实现,如果链表很长(比如几千上万)必须改成循环实现:
//递归实现,简洁 但容易栈溢出
    public static void reversePrint(Node head) {
        if(head == null)
            return;//递归到尾节点
        Node current = head;
        reversePrint(current.next);
        System.out.print(current.element + " ");
    }
3、删除指定节点
同样没有必要遍历前面所有的节点(尾节点除外),只需要把后一个节点复制指定节点即可(复制包括数据域,和指针)。但如果待删除的是尾节点,此时只能进行遍历。实现方式如下:
/**
     *删除指定节点
     * @param head 头节点
     * @param current 待删除节点
     */
    public static void deleteCurrent(Node head, Node current) {
        if(head == null || current == null)
            return;
        if(current.next != null) { //current不是尾结点 复制后一个节点即可
            current.element = current.next.element;
            current.next = current.next.next;
        }else if(head == current) { //只有一个结点,必须在current.next != null之后
            head = null;
            current = null;
        } else { //是尾节点,只能遍历获取到倒数第二节点,然后把该节点的next指针置为null
            Node node = head;
            while(node.next != current) {
                node = node.next;
            }
            node.next = null;
        }
    }
如果是双向链表,删除尾节点就很简单,直接通过尾节点的pre指针就可以找到倒数第二节点,把倒数第二个节点的next指向null,待删除的尾节点的pre指向null即可,具体可以参考java api中LinkedList的unlink方法。

另外还有一些关于链表的常见问题:
A、判断链表是否存在环?定义快慢两个指针,如果相交证明存在环
B、判断两个相交链表的是否相交,并求交点?遍历求得链表的长度差值s,定义快慢指针,快的指向长链表 慢指针指向短链表,快指针先走s步,然后慢指针一起走,第一个相同的节点就是交点。
C、求链表的倒数第K的节点?同样是定义快慢两个指针指向首节点,快指针先走k步,然后与慢指针一起走。当快指针指向尾部时,慢指针指向的节点就是倒数第k个节点。

还有诸如"反转一个链表"等一系列其他问题,这里不再一一讲解。但可以看出链表相关的问题,本质上就是指针问题,搞清楚指针各种指向、以及改变指针的指向是关键。



猜你喜欢

转载自blog.csdn.net/gantianxing/article/details/79853512