大话数据结构 - 链表

代码GitHub地址

链表概述

  • 数组和链表都是线性存储结构的基础实现,栈和队列都是线性存储结构的应用

数组优缺点

说起链表必须说一下数组:数组是一种连续存储线性结构,元素类型相同,大小相等

数组优势:存取速度快。

数组劣势:

  1. 事先必须知道数组的长度
  2. 空间通常是有限制的
  3. 需要大块连续的内存块
  4. 插入删除元素的效率很低

链表优缺点

链表是离散存储线性结构

n个节点离散分配,彼此通过指针相连,每个节点只有一个前驱节点,每个节点只有一个后续节点,首节点没有前驱节点,尾节点没有后续节点。

链表优点:

  1. 空间没有限制
  2. 插入删除元素很快

链表缺点:

  1. 节点的获取很慢

链表分类

  • 对于链表,如果用数组实现。那即是静态链表
  • 链表分好几类:
    • 有单向链表,
      • 一个节点有一个指针域属性,指向其后继节点,尾节点的后继节点为NULL。
    • 双向链表
      • 一个节点两个指针域属性,分别指向其前驱,后继节点
    • 循环链表
      • 相比于单向链表,尾节点的后继节点为链表的首节点。
    • 双向循环链表
      • 能通过任何一个节点找到其他所有节点,相比于双向链表,把最后一个节点的后继节点指向了第一个节点,进而形成环式循环。

链表需要注意的点

  • 链表需要相同的数据类型
  • 链表的处理方式都是先取代,后目的。比如删除链式线性表的某个节点的流程,先取代delete point使它的前驱节点指向它的后继节点,这样就完成了取代。然后再free()掉这个节点,这样就达到了目的。再比如加入某个节点,先使add point指向add index要加入处的后继节点,这即取代。然后再使add index的前驱节点指向add point。
  • 操作一个链表只需要知道它的头指针就可以做任何操作了

解题步骤:

  1. 先考虑特殊情况,边缘值
  2. 进入退出循环时需要找准条件,考虑清楚退出循环时所需要的变量的终值是多少,方便使用
  3. 审视全局,带正确的值判断是否AC

控制选择的节点

int i = 1;

while (i < index) {
    headNode = headNode.getNextNode();
    i++;
}

这段代码意义即,从初始化i=0开始,如果要插入节点到第i个的位置。那么我们就一定需要找到第i-1个节点。所以现在我们目标明确了,要找第i-1个节点。
因为循环的结束条件是由i控制的,如果我们使变量i原本从0开始计数++到i-1结束。跳过的是i个元素。这样就把我们需要的第i-1节点跳过了。而我们要想取到它,很明显我们需要让循环少进行一次。
为了逻辑清晰,循环判断条件不应改变。那么我们就需要把i的初始值改为1,而非原先的0。

链表操作

目录:

  • 遍历
  • 查找
  • 清空
  • 销毁
  • 求长度
  • 排序
  • 删除节点
  • 插入节点

Github – 单链表代码地址 – 欢迎star

添加数据到链表中

  • 那么无疑是找到尾节点插入即可
    while(tempNode.getNextNode()!=null)当退出循环时,那么就定位到了后继节点为NULL的节点即尾节点。

遍历链表

  • 从头结点开始输出,直到尾节点。
    while(tempNode!=null)那么退出循环时即所有节点全部输出。

给定位置插入节点到链表中

  • 链表插入的题要看清题意。是要往第i个位置插入,还是往索引为i的位置插入。在同样具有虚拟头结点的前提下,两者直接决定了i的初始值。
  • 因为要往指定位置插入节点。比如2143要往第3个位置插,那么即插到1,4中间。 所以要往哪个位置插那么必须找到他的前驱节点1。这也是我们前面所讲的控制选择的节点的使用场景。
  • 如下进入判断条件中,如果起始条件是0的时候,那么需要注意,每进入一次循环+1。那么它总共会进入3次。0,1,2。可是循环中节点向下遍历。如果首次进入循环的开始节点是第一个节点而非头结点的话即2。往后跳三次,就会找到3节点,很明显不是我们想要的。
  • 所以我们应该明白如果要插入到第10个位置,我们需要找到第九个节点,那么循环起始从第一个开始的话,找到第九个需要进循环8次退出循环时才是第9个节点。所以我们可以看出,要插到第10个位置我们需要循环得是8次而不是9次。
    • 我们知道如果i从0- i<10是10次。即0-9那么我们可以控制i从i = 1开始,这样只使循环少了一次。那么另一次怎么减呢
    • 我们可以再通过使第一次进入循环的节点是头结点而不是第一个拥有实际value的节点开始。这样我们就可以使无用节点也占用一次循环,也就相当于使循环次数-1了。
    index = 3;
    ----
    int i = 0;
    temp = headNode.getNextNode();
    while(i < index){
        tempNode = tempNode.getNextNode();
        i++;
    }
    
    int i = 1;
    temp = headNode;
    while(i < index){
        tempNode = tempNode.getNextNode();
        i++;
    }
  • 然后接下承上,即要插入的这个节点Node.next = 4然后1.next = Node。切不可乱了顺序,否则会丢失指针

获取链表的长度

  • 遍历链表,声明一个变量,每遍历一个节点变量+1。
int i = 0;
tempNode = headNode.next;
while(tempNode!=null) {
    i++;
    tempNode = tempNode.next;
}
  • 这里需要明白两点。
    • 当tempNode等于头结点的时候,我们可以想到这种情况会使循环多进行一次。
    • 当循环判断条件while(),判断的不是tempNode!=null即当前节点,而是tempNode.next!=null下一节点时。会使循环少进行一次。
    • 这两点很重要,循环进行多少次的掌控就在这几点。

删除给定位置的节点

  • 和从指点位置插入点一样。都是要找指定位置的前驱节点。
  • 总共4个节点,我们要删除第3个位置的节点。即需要找到第2个节点,进入一次循环节点向后移一位, 进入节点时是第一个节点。那么我们需要进循环一次即可。那么即
int i = 1;
while(i < 3){
    i++;
    temp = temp.next;
}

对链表进行排序

  • 冒泡排序
    • 双层循环,外层节点起始位置第一节点temp = headNode.next,退出条件temp != null。这是索引循环起始指向第一节点,最终从末尾节点跳出。
    • 内层循环,起始节点即第一节点nextNode = headNode.next,退出条件nextNode.next != null,因为冒泡是前后元素比大小第1和第2比,第n-1和第n比。所以当指到最后节点的时候就可以跳出。

找到链表中倒数第k个节点

  • 这个起始和正向找节点是一个概念,我们只要利用循环链表的思路变换数字即可。首先我们可以把链表看成一个首尾相连的循环结构。那么倒数第k个,即一个元素正数位置的绝对值加上倒数位置的绝对值比总数多1。比如6个数,我们找倒数第二个。倒数第2个也是正数第5个,2+5-1=6。所以我们要找一个数,正向找无疑很方便,只需要(-k+1+总数) = 元素位置。我们只需要对链表进行排序后获取该元素即可
/**
     * 找链表倒数第index个节点
     *
     * @param headNode
     * @param index
     * @return
     */
    public static int findNode(Node headNode, int index) {
        int listLength = listLength(headNode);
        index = -index;
        int trueIndex = (index + listLength + 1) % listLength;
        for (int i = 0; i < trueIndex; i++) {
            headNode = headNode.getNextNode();
        }
        return headNode.getValue();
    }
  • 我们也可以用追随节点办法,设立两个节点。一个节点比另一个节点快k步。即先行节点走了k下,后行节点出发走第一下。当后行节点跳出循环,那么先行节点必然是倒数第k个节点。我们一般也用这种方法来找一个链表的几等分点。如果找一个链表的三等分点,我们给连个节点一个走三步一个走一步即可。
/**
     * 找倒数第k个节点|追点,简洁写法
     *
     * @param headNode
     * @return
     */
    public static int findNode3(Node headNode, int k) {
        Node tempNode = headNode;
        Node nextNode = null;
        int i = 0;
        while (tempNode != null) {
            i++;
            tempNode = tempNode.getNextNode();
            if (tempNode == null) {
                break;
            }
            if (i == k) {
                nextNode = headNode.getNextNode();
            } else if (i > k) {
                nextNode = nextNode.getNextNode();
            }
        }
        return nextNode.getValue();
    }

删除链表重复数据

这里我用while循环写的,比较简单而且简洁。其实本质上和冒泡排序的思路一样,思考的时候用for循环的思路思考临界条件即可。非常简单

  • 双重循环,第一层循环是索引节点循环,循环进入从第一个节点开始,到尾节点跳出。
    • 内循环,每进入内循环一层循环的时候把next节点赋予最新的外层索引值。然后取该next节点的next节点值与索引节点的值进行比较。向后循环
/**
 * 删除重复元素,|参考别人的 O(n2)
 *
 * @param headNode
 */
  public static void removeDumplecateEle(Node headNode) {
        Node temp = headNode.getNextNode();
        Node next;
        while (temp != null) {
            next = temp;
            while (next.getNextNode() != null) {
                if (next.getNextNode().getValue() == temp.getValue()) {
                    next.setNextNode(next.getNextNode().getNextNode());
                } else {
                    next = next.getNextNode();
                }
            }
            temp = temp.getNextNode();
        }
    }
  • hashtable做法。使链表上的每个节点插入到hashtable中,插入前进行判断是否在hashtable中存在。
    • 需要注意的是,hashtable在存储的时候是将该值存到了键值的位置。value恒定给值,否则不方便遍历。
/**
     * 删除重复元素 hashtable| 空间开销变大,O(n)
     *
     * @param headNode
     */
    public static void removeDuplecate3(Node headNode) {
        Hashtable<Integer, Integer> hashtable = new Hashtable<Integer, Integer>();
        Node tempNode = headNode.getNextNode();
        while (tempNode != null) {
            if (!hashtable.containsKey(tempNode.getValue())) {
                hashtable.put(tempNode.getValue(), 1);
            }
            tempNode = tempNode.getNextNode();
        }
        // 打印
        for (Map.Entry<Integer, Integer> entryItem : hashtable.entrySet()
                ) {
            System.out.println(entryItem.getKey());
        }
    }

查询链表的中间节点

  • 两个指针,一个走两步,一个走一步。同一起点出发,拿先行节点进行判断null值,先行节点为null那么跳出循环,此时后行节点即为中间节点。

递归从尾到头输出单链表

  • 很简单,因为链表上来我们只知道头结点。而尾遍历,尾节点的特点就是next=NULL。所以我们只需要来个循环递归的找到递归到尾节点即可。然后方法栈由里到外依次打印即可。
 /**
     * 链表逆序递归遍历
     *
     * @param headNode
     */
    public static void traverseByRecursive(Node headNode) {
        if (headNode != null) {
            traverseByRecursive(headNode.getNextNode());
            if (headNode.getValue() != HEAD_POINT_NUMBER) {
                System.out.println(headNode.getValue());
            }
        }
    }

反转链表

  • 递归和非递归的思想差异。递归是从链表尾开始两两个体相互反转。而非递归实现是从链表头开始两两个体相互反转。
  • 非递归
    • 非递归实现需要注意,每一步我们做了什么。比如4,2,3,3。我们第一步拿出了4,第二步成了2,4。第三步3,2,4。这样。所以我们应该做的是,
      • 首先声明一个节点tempNode,让其根据链表循环向后遍历链表。直到=null。这样我们所有元素都反转了。
      • 其次我们发现第一个元素抽出的时候,即新链表的头我们把他进行标识firstNode以方便为后续抽出的节点的next属性赋值。比如4,它的next元素是null而后抽出的元素他们的next元素都是在它前一位抽出的元素。因为这个next元素的赋值操作发生在循环中,且只有一次是null其他几次都为实际元素,所以我们需要声明一个节点nextNode初始化为null。当他第一次进入循环的时候,即拿出4的时候将其放入tempNode的next属性中。
      • 然后赋值操作后紧跟着对该nextNode元素进行更改,让其等于新链表的头firstNode。
while (tempNode != null) {
       firstNode = tempNode;
       tempNode = tempNode.getNextNode();
       firstNode.setNextNode(nextNode);
       nextNode = firstNode;
}
  • 递归

    • 程序分两种情况处理,首先做特殊处理,当链表只有一个节点,或者说该节点为null的时候。那么返回该节点。
    • 当链表含有多个元素的时候,递归依次遍历该链表直到当前链表的最后一个节点(退出条件由第一步控制)。最深层递归函数为次深层递归函数返回当前链表的最后一个节点。
    • 尾元素的next指向他原链表的前驱节点。前驱节点的next指向null
    • 递归从最深层返回的头依次传递在递归函数间,从最深层传到最顶层。供最外层函数返回。

    递归需要注意的是:递归时,每次退出一层递归函数,headNode节点都会前移一位。因为最深层的headNode节点什么都没做,只是找到了表尾节点进行返回。

    Node temp;
        if (headNode == null || headNode.getNextNode() == null) {
            temp = headNode;
        } else {
            // A B C- D-
            // temp = D;
            // headNode = C;
            // indexNode = D;
            // A -> B -> C          D -> C -> null
            Node indexNode = recursiveReversalLinklist(headNode.getNextNode());
            headNode.getNextNode().setNextNode(headNode);
            headNode.setNextNode(null);
            temp = indexNode;
        }
        // 反回新链表的头,递归多层函数间重复传递,
        return temp;
    

单链表的扩展

  • 循环链表
  • 双向链表

都很简单,不赘述了。

    原文作者:HikariCP
    原文地址: https://www.jianshu.com/p/2df7d0af2e82
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞