zoukankan      html  css  js  c++  java
  • 链表(1) --- 单链表

      链表,和数组一样,也是一种线性的数据结构。但链表在存储数据的时候,却不像数组把所有的数据都存储在一片连续的内存空间中,而是数据分散在内存中,数据和数据之间相互链接。数据和数据怎么才能相互链接?比如,5和10怎么才能进行链接?很显然,仅仅依靠数据本身是无法链接起来的,还需要地址。存储数据的时候,同时存储一个地址,当再存储一个数据的时候,把该数据的地址赋值前一个数据中的地址,只要知道地址,就能找到元素,也就相当于,数据和数据链接起来了,所以链表在存储数据的时候,不仅要存数据本身,还要存地址。也正因为如此,链表存储的是一个对象。对象的一个属性是数据,一个属性是地址。链表是对象与对象之间的链接。但地址怎么获取呢?只要链表存储的是对象,地址就很好获取了。在Java 和JavaScript中,当创建一个对象,并把这个对象赋给一个变量的时候,这个变量保存的是就这个对象的地址。当把这个变量复制给另外一个变量的时候,复制的是对象的地址,也就是说,两个变量同时指向了一个对象。也就是说,链表中的每一个对象中,它的地址属性只要声明成对象类型的变量就可以了。链表是对象与对象的链接,这也引出了另外一个问题,链表是存储在堆内存中的(因为对象都是存储在堆内存中的),怎么才能操作它呢?只能把链表中的第一个对象赋值给一个变量,操作链表的时候,通过这个变量去找到链表中的第一个对象,进而找到整个链表进行操作。也就是说,链表中始终都要存在一个指向链表第一个对象的变量,用来找到链表,进而操作链表。链表中的每一个对象也称为节点, 指向链表第一个对象的变量称为链表头。一个简单的链表如下:

      链表的分类有三种,单向链表(单链表),双向链表,循环链表。

      单向链表:链接方向是单向的,一个节点只能链接它的后一个节点,而不能链接它的前一个节点,链表的最后一个节点链向null。对链表的访问只能从头部开始,依次向后顺序访问,像上图一样。

      双向链表,链接方向是双向的,就是它的每一个节点中都有两个地址(假设是prev和next),prev指向它的前一个节点, next指向后一个节点,第一个节点的prev提向null,最后一个节点next指向null

      

      循环链表就是,链表的最后一个元素不是指向null,而是指向第一个结点。不管是单向链表还是双向链表,它都能变成循环列表

       

     链表的实现方式也有两种方式,一种是不带头结点,链表头指向的第一个元素就是真实的数据节点

      一种是带一个头节点,头节点并不保存任何有效数据,链表头指向头节点,头节点指向链表中真正有意义的第一个节点。

       先看单向链表,由于链表中的每一个节点都是对象,所以要声明一个类来表示这个对象,由于这个类仅在单向链表中使用,可以声明为私有内部类,类的属性分为数据和地址,为了简单起见,数据使用整数,地址就是节点类的变量。为了能够操作链表,还需要一个变量,让它指向链表中的第一个节点,所以一个单向链表要有两个属性,一个是内部节点类,一个是指向链表的第一个元素。有时还要计算链表中节点的个数,再加上一个属性size

    public class SingleLinkedList {
    
        private class Node { // 链表的节点类型
            int data; // 数据
            Node next; // 地址
    
            Node(int data){
                this.data = data;
            }
        }
    
        private Node head; // 指向链表的第一个元素,用来找到链表,操作链表
        private int size; // 记录节点的个数
    }

      可以想到,这样的实现方式是不带头节点的,因为head默认初始化为null,它并没有指向一个节点。

      插入节点,可以从链表的头部插入,可以从链表的尾部插入,还可以从链表的中间插入,还可以在某个节点后面插入,相应的,你可以单独提供方法,也可以提供通用的方法。头部插入最为简单,就是创建新的节点,让新的节点next指向head(把head赋值给新节点的next属性),然后再让head指向新的接点(把新节点赋值给head),最后不要忘记size++;

    public void insertFirst(int data){
        // 创建新的节点
        Node newNode = new Node(data);// 新的节点的next指向head
        newNode.next = head;
        // 让head指向新的节点
        head = newNode;

    size++; }

      从尾部插入节点,就有点麻烦了,因为要先找到尾节点,然后再让尾节点的next 指向新的节点。怎样才能找到尾节点?尾节点的next 是null,只要找到一个节点的next 属性是null, 它就是尾节点。如果head.next 是null,则head是尾节点,如果head.next.next 是null,那head.next 就是尾节点。找一个临时变量temp, 把head赋值给它, 那么temp 也就是指向了第一个节点,判断temp.next 是不是null, 如果是,它就是尾节点,如果不是,那就把temp.next 再赋值给temp, 这时temp就指向第二个节点。如果temp.next 还不是null, 那就再把temp.next 赋值给temp, 这时temp指向第三个节点。 可以发现,就是不停地把temp.next 赋值给temp,让temp 指向下一个节点,直到temp.next 是null,那temp 就是最后一个节点,这时把新的节点赋值给temp.next。赋值成功后,不要忘记size++。尾部插入节点要注意的是,先判断head是否为null,如果head是null,head赋值给temp,temp.next就会报错。head为null,链表为空,也没有必要循环链表。

     

    public void insertLast(int data) {
        // 新node的next默认是null
        Node newNode = new Node(data);
    
        if (head == null){
            head = newNode;
        } else{
            Node temp = head;
            // 只有node的next的节点是null, 才是最后一个节点。
            while (temp.next != null) {
                temp = temp.next;
            }
            // temp是最后一个节点
            temp.next = newNode;
        }
    size++; }

      在链表的中间位置或任意位置插入节点,那就要先定位到这个位置,找到这个位置的前一个节点,因为单链表只能链接到下一个节点,要在某个位置插入节点,肯定要让这个位置之前的那个节点的next指向新的节点,同时让新的节点的next指向原来前一个节点指向的后一个节点。定位位置,找到前一个节点,肯定是要循环遍历了,遍历到指定的位置。插入到第n个位置,遍历n-1次,得到的节点,就是前一个节点。

      理论上是这样,但实现上还要复杂,因为有好多情况要判断,链表是否为空,如果为空,那就只能插入第一个节点。插入的位置是否符合要求,这里要确定一下,链表的第 一个节点是位置0还是位置1,如果按照数组的规则,链表中的第一个节点就是位置0。那么要插入的位置范围就是[0, size]. 如果链表中只有一个节点,是可以插入到位置1的,位置0的节点next指向这个节点就可以了。

    public void insert(int position, int data) {
            // 创建新节点
            Node newNode = new Node(data);
    
            // 判断链表是否为空
            if(head == null) {
                // 如果为空,那只允许插入到第一个节点,也就是位置0处,否则报错
                if (position != 0){
                    throw new RuntimeException("链表为空");
                } else {
                    head = newNode;
                }
            } else { // 至少有一个节点
                // 判断位置是否符合要求
                if(position < 0 || position > size) {
                    throw new RuntimeException("超出范围");
                } else {
                    // 插入头节点
                    if(position == 0) {
                        insertFirst(data);
                    } else if (position == size) { // 插入尾节点
                        insertLast(data);
                    } else {
                        Node prevNode = head;
                        // 注意是position -1,如果插入到1位置,那head就是前一个节点,无需循环,position-1 -> 0, 不循环
                        for (int i = 0; i < position -1; i++) {
                            prevNode = prevNode.next;
                        }
                        // prevNode就是前一个节点。
                        // nextNode是后一 个节点
                        Node nextNode = prevNode.next;
                        // 新节点的链接:前一个节点的next指向新节点,新节点的next指向后一个节点
                        prevNode.next = newNode;
                        newNode.next = nextNode;
                    }
                }
            }
         size++; }

      插入功能完成后,就要验证实现的对不对,简单的办法就是把链表中的数据打印出来,遍历整个链表。找一个临时变量curNode, 把head赋值给它, 那么curNode也就指向了第一个节点,此时判断curNode是不是null, 如果是,链表为空,什么都不用做了。如果不是,把curNode的data打印出来,再把curNode.next 赋值给curNode, 这时curNode就指向第二个节点。再判断curNode是不是null, 如果不是,继续打印它的data,把curNode.next 赋值给curNode, 这时curNode指向第三个节点。 可以发现,就是不停地判断curNode是否为null, 如果不是,那就打印data,并curNode.next 赋值给curNode,让curNode指向下一个节点,直到curNode 为null。这里要注意它和插入尾节点遍历的不同,插入尾节点,是判断.next是否为null,这里只判断节点是否为null

    public void display() {
            Node curNode = head;
    
            while (curNode != null) {
                System.out.print(curNode.data);
                curNode = curNode.next;
            }
        }

      删除节点,删除节点也有很多情况,删除头节点,删除某个位置节点,删除某个指定的节点。删除头节点比较简单,直接让head指向head.next 就可以了,删除方法通常会返回要删除的节点

     

    public Node deleteFirst() {
        if (head == null) {
            throw new RuntimeException("链表为空");
        }
        Node firstDeleted = head;
        head = head.next;
        return firstDeleted;
    }

      删除某个指定的节点,分为三种情况,这个节点正好是头节点,这个节点在中间或尾节点,这个节点没有找到。当删除某个节点时,不仅要找到这个节点,还要找到这个节点的上一个节点,还是因为链表是单向的,删除某个节点,就要让某个节点的上一个节点的next指向这个节点的next指向的下一个节点,那遍历查找某个节点时,还要记录前一个节点。

    public Node deleteKey(int key){
        if (head == null) {
            throw new RuntimeException("链表为空");
        }
    
        Node curNode = head;
        Node prevNode = null;
    
        // 删除的某个key正好位于头节点中
        if(curNode.data == key){
            head = curNode.next;
            return curNode;
        }
    
        // 删除的某个key位于中间节点或尾节点
        while (curNode != null && curNode.data != key) {
            prevNode = curNode; //记录前一个节点
            curNode = curNode.next;
        }
    
        // 如果找到了
        if(curNode != null){
            prevNode.next = curNode.next;
            return curNode;
        }
    
        // 如果没有找 curNode == null
        System.out.println("没有找到");
        return null;
    }

      删除某个位置的节点,要先找到这个节点,同时记录上一个节点。怎么才能找到这个节点呢?遍历到该位置。

    public Node deleteByPostion(int postion) {
        if (head == null) {
            throw new RuntimeException("链表为空");
        }
        
        if(postion < 0 || postion >= size) {
            throw new RuntimeException("位置不对");
        }
        
        if(postion == 0){
            Node first = head;
            head = head.next;
            return first;
        }
    
        Node curNode = head;
        Node prevNode = null;
        for (int i = 0; i < postion; i++) {
            prevNode = curNode;
            curNode = curNode.next;
        }
        
        prevNode.next = curNode.next;
        
        return; curNode;
    }

      查找

    public int find(int data){
        Node curNode = head;
        int index = 0;
        boolean isFind = false;
    
        while (curNode != null){
            if(curNode.data == data){
                isFind = true;
                break;
            } else {
                index++;
                curNode = curNode.next;
            }
        }
    
        if(isFind) {
            return index;
        } else {
            return -1;
        }
    }

      

  • 相关阅读:
    吴裕雄 Bootstrap 前端框架开发——Bootstrap 字体图标(Glyphicons)
    Logical partitioning and virtualization in a heterogeneous architecture
    十条实用的jQuery代码片段
    十条实用的jQuery代码片段
    十条实用的jQuery代码片段
    C#比较dynamic和Dictionary性能
    C#比较dynamic和Dictionary性能
    C#比较dynamic和Dictionary性能
    分别使用 XHR、jQuery 和 Fetch 实现 AJAX
    分别使用 XHR、jQuery 和 Fetch 实现 AJAX
  • 原文地址:https://www.cnblogs.com/SamWeb/p/14945837.html
Copyright © 2011-2022 走看看