数据结构之单向链表

    最近在研究Handler机制的时候发现Java层的消息队列MessageQueue是通过单向链表的数据结构来管理各个Message的,考虑到单向链表是一种很基础的数据结构,不掌握的话太不像话了,所以系统性的研究了单向链表。

    单向链表的含义非常简单,他就是一系列的节点(Node)单向的串联,每个节点都有他的下线(谍战片看多了),下线节点用next表示(不是强制的,但是是约定俗成的);两个节点之间单向联系,上线节点能够通过next直接找到他的下线节点,但是下线节点是没有办法直接找到他的上线节点的:
数据结构之单向链表_第1张图片
jd1.png
    如上图所所示,每个节点包含两个成员变量:data和next(这里只是举一个最简单的例子,实际上有多少个成员变量,视需求而定),data可以是任意类型的,视需求而定;next就是node类型(node到底是什么类型,也是视需求而定),每一个节点的next指向他的下线节点,所以拿到一个节点后,可以直接拿到他的下线节点,如果下线节点不为空的话,一般把链表的第一个节点称为头节点,头节点没有上线节点,对于一个链表,拿到了他的头节点就相当于拿到了这个链表;最后一个节点称为尾节点,尾没有下线节点,其他的节点都会有下线节点,如果发现某个节点的next为空,即node.next = null,说明这个节点就是尾节点,这在遍历链表的时候经常用到;需要注意的是,在内存中,各个节点的内存地址不是连续的,而是离散的。

    针对链表的节点操作主要是增、删、查,下面 通过一个Demo来实际操作一遍,最后结合MessageQueue,研究下Android是怎么通过链表来管理Message的,这对研究Handler机制也是有意义的,先来看Demo:

/**
 * 测试单向链表
 * @author tushihao
 *
 */
public class TestLinked {

    /*
     * 链表长度
     */
    private int length;

    /*
     * 链表的头节点
     */
    private Node header;

    /**
     * 获取链表的长度
     * 
     * @return  返回链表长度
     */
    public int getLenght() {
        return length;
    }

    /**
     * 获取链表的头结点
     * 
     * @return  返回链表头结点
     */
    public Node getHeader() {
        return header;
    }

    public static void main(String[] args) {
    
    }

    /**
     * 打印整个链表的内容
     */
    private void showLinked() {
        String lk = "";
        Node tmp = header;
        if (length == 0) {
            lk += "the linked is empty";
        } else {
            while (tmp.next != null) {
                lk += tmp + "---> ";
                tmp = tmp.next;
            }
            lk += tmp;
        }
        System.out.println(lk);
    }

/**
 * 节点类,类名随意
 * 
 * @author tushihao
 *
 */
class Node {
    //创建链表肯定是为了保存数据,所以每个节点要有一个成员变量来保存数据
    public Object data;
    
    //下线节点
    public Node next;

    public Node(Object object) {
        data = object;
    }

    @Override
    public String toString() {
        return "Node[" + data + "]";
    }
}

    1.增加节点:

    /**
     * 将目标节点插入链表指定的位置
     * 
     * @param node  待插入的节点
     * @param index  节点要插入的位置;0代表插入链表头;-1代表插入链表尾;大于0插入大于0的位置
     */
    public void addNode(Node node, int index) {
        if (node == null || node.data == null) {
            return;
        }
        
        //两个中间变量
        Node tmp = header;
        Node prev = null;
        
        // 如果插入的位置是链表头
        if (index == 0) {
            if (length == 0) {
                //如果链表为空,那么直接将目标节点赋值给头节点
                header = node;
            } else {
                //首先将目标节点赋值给头节点,然后将目标节点的下线节点指向原来的头节点
                header = node;
                header.next = tmp;
            }
        } else if (index == -1) {
            // 如果要插入链表尾,那么遍历整个链表
            while (tmp.next != null) {
                tmp = tmp.next;
            }
            // while循环完成之后,tmp就指向了链表的尾节点,将tmp的
            //next指向待插入的节点即可,这样待插入的节点就成了尾节点
            tmp.next = node;
        } else {
            // 如果要插入任意指定的位置,那么需要记录那个位置的前一个节点,因为前一个节点的next就是
            // 待插入的节点,这个连接关系要接上,定义leng就是为了判断有没有遍历到目标位置
            int leng = 0;
            // 如果目标位置比链表长度还大的话,可以直接返回,也可以插入链表末尾,视需求而定,这里直接返回
            if (index > length) {
                return;
            }
            while (tmp.next != null) {
                // 如果遍历到目标索引了,那么首先记录该节点,因为目标节点的next就是这个节点,
                //同时还要记录这个节点上线节点,因为上线节点的next就是目标节点,这里涉及到三个节点
                if (leng == index) {
                    prev.next = node;
                    node.next = tmp;
                    break;
                } else {
                    leng++;
                    prev = tmp;
                    tmp = tmp.next;
                }
            }
        }
        length++;
    }

    测试代码:

TestLinked tl = new TestLinked();
tl.addNode(new Node("F"), 0);
tl.addNode(new Node("E"), 0);
tl.addNode(new Node("D"), 0);
tl.addNode(new Node("C"), 0);
tl.addNode(new Node("B"), 0);
tl.addNode(new Node("A"), 0);

System.out.println("原始链表长度:" + tl.length + " , 链表内容:");
tl.showLinked();

// 往链表头插入一个节点
tl.addNode(new Node("tuhao"), 0);
System.out.println("将tuhao插入头节点,插入后长度:" + tl.length + " , 链表内容:");
tl.showLinked();

//往链表尾插入一个节点
tl.addNode(new Node("dana"), -1);
System.out.println("将dana插入尾节点,插入后长度:" + tl.length + " , 链表内容:");
tl.showLinked();

//往第三个位置插入一个节点
System.out.println("将china插入第3个位置");
tl.addNode(new Node("china"), 2);
System.out.println("插入后的长度:" + tl.length + " , 链表内容:");
tl.showLinked();

    测试结果:

原始链表长度:6 , 链表内容:
Node[A]---> Node[B]---> Node[C]---> Node[D]---> Node[E]---> Node[F]
将tuhao插入头节点,插入后长度:7 , 链表内容:
Node[tuhao]---> Node[A]---> Node[B]---> Node[C]---> Node[D]---> Node[E]---> Node[F]
将dana插入尾节点,插入后长度:8 , 链表内容:
Node[tuhao]---> Node[A]---> Node[B]---> Node[C]---> Node[D]---> Node[E]---> Node[F]---> Node[dana]
将china插入第3个位置
插入后的长度:9 , 链表内容:
Node[tuhao]---> Node[A]---> Node[china]---> Node[B]---> Node[C]---> Node[D]---> Node[E]---> Node[F]---> Node[dana]

    2.删除节点:

    /**
     * 根据内容删除指定的节点
     * 
     * @param object  待删除的节点的内容
     * @return 删除的节点的索引,如果没有删除成功,那么返回-1
     */
    private int removeNode(Object object) {
        // 空链表当然不存在移除的问题
        if (length <= 0 || object == null) {
            return -1;
        }

        // 定义三个中间变量
        Node tmp = header;
        Node prev = null;
        // 目标节点的索引
        int index = -1;

        // 首先判断是不是移除头节点
        if (object.equals(header.data)) {
            index = 0;
            header = header.next;
            length--;
            return index;
        }
        // 遍历整个链表,对比data,符合条件就移除
        while (tmp.next != null) {
            // 判断相等是用==还是equals亦或用别的方法,视需求而定,
            //本例中的object是String类型,所以用equals
            if (object.equals(tmp.data)) {
                // 所谓的移除,就是将目标节点的前一个节点的next指向目标
                //节点的下一个节点,这样这个节点就没有上线节点和下线节点

                // 将目标节点的前一个节点的next指向目标节点的后一个节点
                prev.next = tmp.next;
                length--;
                return index;
            } else {
                prev = tmp;
                tmp = tmp.next;
                index++;
            }
        }
        // 如果没有指定的节点,那么返回-1
        return index;
    }

    测试代码:

System.out.println("tuhao太高调了,把这个节点移除掉");
int i = tl.removeNode("tuhao");
System.out.println("目标节点索引:" + i + ",移除后的长度:" + tl.length + " , 链表内容:");
tl.showLinked();

    测试结果:

tuhao太高调了,把这个节点移除掉
目标节点索引:0移除后的长度:8 , 链表内容:
Node[A]---> Node[china]---> Node[B]---> Node[C]---> Node[D]---> Node[E]---> Node[F]---> Node[dana]

    3.查找节点:

    /**
     * 根据指定内容查找对应的节点(也可以根据索引查找指定的内容)
     * 
     * @param object  查找的条件
     * @return 返回节点,没有找到的话,返回null
     */
    private Node findNode(Object object) {
        Node result = null;
        if (length == 0 || object == null) {
            return result;
        }
        Node tmp = header;
        while (tmp.next != null) {
            if (object.equals(tmp.data)) {
                result = tmp;
            } else {
                tmp = tmp.next;
            }
        }
        // 目标节点可能是最后一个节点,所以最后一个节点要单独判断
        if (object.equals(tmp.data)) {
            result = tmp;
        }
        return result;
    }

    测试代码:

System.out.println("查找dana");
System.out.println(tl.findNode("dana"));

    测试结果:

查找dana
Node[dana]

    上面的Demo应该是自从改革开放以来最简单的单向链表使用的例子,仅适合入门;下面结合源码来分析单向链表在Android中的运用。
    Handler相信每个从事Android开发的人都用过,应用进程通过sendMessage或者其他的方法将Message发送到framework后,framework就是通过单向链表这种数据结构来管理各个Message的,代码如下:

路径:frameworks/base/core/java/android/os/MessageQueue.java

public final class MessageQueue {
    ......
    //MessageQueue有个Message类型的成员变量,这个成员变量mMessage
    //就是单向链表的头节点,上面也说过,拿到头节点就相当于拿到了单向链表
    Message mMessages;
    ......
    
    //sendMessage等方法最终会走到enqueueMessage这个方法
    boolean enqueueMessage(Message msg, long when) {
        //target是Message中Handler类型的成员变量,因为每个Message
        //被Handler发送出去后,最终还是要回到Handler的handleMessage
        //方法处理的,所以和Message绑定的Handler肯定是不能为空的
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        
        //如果要发送的Message已经在消息队列里面了,那么不能再次发送此Message
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + 
                    " This message is already in use.");
        }

        synchronized (this) {
            //mQuitting默认是false,只有当Looper退出的时候才会把它置为true
            //Looper退出了,意味着这个Handler被废弃了,不用了
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                             msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            //首先将此Message添加正在使用的标记为
            msg.markInUse();
            //给Message的when属性赋值,这个属性很重要,
            //他决定了此Message在单向链表中的位置
            msg.when = when;
            //首先将头消息赋值给局部变量p
            Message p = mMessages;
            //是否需要唤醒主线程
            boolean needWake;

            //p指向是的是消息读队列的头Message,p == null说明该队列里面目
            //前没有Message,发过来的Message就是头Message;when == 0说明
            //发过来的消息是马上需要处理的Message(有些Message可能存在delay)
            //when < p.when说明当前消息队列的Message全是delay的Message,而
            //发过来的Message没有delay,或者delay的时间比那些Message短,此时
            //就应该优先处理这种Message,这种Message就应该放到链表的头,因为Looper
            //是从链表的头Message开始获取Message,然后分发给Handler来处理的。
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                //符合上面三个条件的Message就是头Message,原来的头Message
                //就成为了发过来的Message的下线Message(就是上面Demo说的下线节点)
                msg.next = p;
                //将头Message指向发过来的Message
                mMessages = msg;
                //是否需要唤醒主线程
                needWake = mBlocked;
            }else {
                // 源码是有注释的,太长了,删掉

               //一旦进入这个分支,说明发过来的Message必须插到链表的某个
               //位置,这个位置不能是头节点,怎么确定这个节点的位置呢?就
               //是把目标Message插入到链表的第二个位置,然后比较第二个Message
               //和第三个Message的when的大小,如果第三个的when小,说明
               //目标Message应该在第三个Message的后面,两者交换位置,接
               //着比较第三个Message和第四个Message的when的大小,依次类
               //推,如果找到了一个Message的when比目标Message的when大的
               //Message,那么停止比较和交换;如果没有找到,那么就插入链表尾

                //是否需要唤醒主线程,不影响链表的分析,暂且不管,在Hanlder专题再说
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    //刚进入循环时,p指向头Message,赋值给prev,prev记录
                    //的是目标Message的上线Message
                    prev = p;
                    //把p指向p的下线Message
                    p = p.next;
                    //p == null代表遍历到节点的尾部了;when < p.when说明目标Message
                    //应该插入到p的前面,这两种情况都应该跳出循环,没必要再循环了
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                //目标Message强行插队,下次循环时如果发现msg的when
                //比msg.next的when大的话,那就打脸了,插队失败,msg
                //往后挪,msg代表的是目标Message,也就是发送过来的Message
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
        }
    }
}

    以上就是单向链表在Android中的使用

你可能感兴趣的:(数据结构之单向链表)