数据结构基础----链表(Linked List)

原文:

https://loubobooo.com/2018/09/16/%E5%88%9D%E5%AD%A6%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84-%E9%93%BE%E8%A1%A8/

1、链表的概括

虽然动态数组靠resize方法解决静态数组固定容量的问题,但依旧摆脱不了仍是静态数组的事实,而链表则与上述线性数据结构都不同,是一种真正的动态数据结构

2、链表为何如此重要

  • 最简单的动态数据结构
  • 更深入的理解引用(或者指针)
  • 更深入的理解递归
  • 辅助组成其他数据结构
    注:链表本身也是有它非常清晰的递归结构的,由于它天身这种递归性质,可以帮助大家更加深入的理解递归机制相应的数据结构。

3、 具体来看看什么是链表

  • 链表,通常数据存储在“节点”(Node)中。
  • 对链表的节点来说只有两部分,一是存储真正的数据,而另一部分是node类型的对象next(next本身又是一个节点,连接起了下一个节点)。

    类比火车,每个节点是一节车厢,在车厢中存储真正的数据。而车厢和车厢还要进行连接,以使得所有数据是整合在一起的。用户可以方便地在对这些数据上查询等进行其他操作,而数据和数据之间连接就是由这个next来完成的。

  • 简单的图示一下

  • 比如,第一个节点存放了元素1,同时它有一个指向next(也就是用一个箭头来表示),指向了下一节点(node)
  • 第二个节点存放了元素2,以此类推……
  • 到最后个节点的next存储的是NULL

4、链表的优缺点

优点

真正的动态,不需要处理固定容量的问题

不必像静态数组一样,需要考虑开辟多少空间出来,同时还要考虑这个空间是否够用。对于链表来说,你需要存储多少个数据,你就可以生成多少个节点,把它们挂接起来,这便是动态的含义

缺点

  • 丧失了随机访问的能力

    链表不能像数组那样,从数组的索引中直接取出元素。在底层机制上,数组所开辟的空间,在内存中是连续分布的,所以可以直接寻找这个索引对应的偏移,直接计算出相应的数据所存储的内存地址,用O(1)的复杂度把这个元素取出来

    但是链表不同。链表是靠next一层层连接,所以在计算机的底层,每一个节点所在的内存位置是不同的。因此必须通过遍历,一层一层找到这个元素,这便是链表最大的缺点。

    5、 数组和链表的对比

  • 数组支持快速查询
  • 链表便是支持动态
  • 如果一个节点的next是NULL,那就说明这个节点一定是最后一个节点,这就是链表。
  • 数组和链表的对比如下图所示:

6、链表的实现

对于链表来说,我们想要访问这个链表中所有的节点,就必须把链表的头(head)存储起来。


代码实现

public class LinkedList<E> {

    private class Node {
        public E e;
        public Node next;

        public Node(E e, Node next) {
            this.e = e;
            this.next = next;
        }

        public Node(E e) {
            this(e, null);
        }

        public Node() {
            this(null, null);
        }

        @Override
        public String toString() {
            return e.toString();
        }
    }

    private Node head;
    private int size;

    public LinkedList(){
        head = null;
        size = 0;
    }
}

6.1、在链表头添加元素

下面我们来看下链表最重要的操作–如何为 链表头 添加元素(数组中的size -1跟踪数组尾部,故在数组尾部添加元素较为方便

,链表中有head跟踪链表头部,而没有指针跟踪尾部,故在头部添加元素较为方便)

  1. 假设,要将666这个元素添加到链表中,
  2. 相应的需要在node节点里存放666这个元素,以及相应的next(指向)
  3. 然后node节点的next指向链表的头,即node.next=head
  4. 最后head也指向存放666的node节点,即head=node

注: 整个过程在一个函数中执行,函数结束之后,相应node变量的块作用域也就结束了
代码实现

public void addFirst(E e){
    Node node = new Node(e);
    node.next = head;
    head = node;
}

6.2、 在链表的中间添加新的元素

现在来处理稍微复杂一点的问题,在链表的中间添加新的元素

  1. 对于这个链表,要在这个链表索引(链表是无索引概念,只是借用索引这个概念来阐述)为2的地方添加一个新的元素666
  2. 首先遍历找到索引为2的前一个节点prev
  3. 然后prev.next指向存放2元素的节点,同时存放666节点node.next也指向它,因此得到node.next=prev.next
  4. 之后存放666节点node挂接起下一个节点,即prev.next=node
  5. 经过这样的操作,完成了对在链表中间添加新的元素

代码实现

// 在链表的index(0-based)位置添加新的元素e
// 在链表中不是一个常用的操作,通常仅供练习
public void add(int index, E e){
    if(index < 0 || index > size){
        throw new IllegalArgumentException("Add failed. Illegal index.");
    }
    if(index == 0){
        addFirst(e);
    }else{
        Node prev = head;
        //i=0 prev指向1;i=1,prev指向2
        for(int i = 0; i < index - 1; i++){
            // 把当前prev存的这个节点的下一个节点放进prev这个变量中
            // prev这个变量在链表中会一直向前移动,直到移动到index-1这个位置
            // 最后就找到了待插入的那个节点的前一个节点
            prev = prev.next;
        }
        Node node = new Node(e);
        node.next = prev.next;
        prev.next = node;
        size++;
    }
}

 6.3、在链表的尾部添加新的节点

 //在链表尾部添加新的运算e
    public void addLast(E e) {
        add(size ,e);
    }

6.4、为链表设立虚拟头节点

在链表添加元素的过程中,我们遇到了在链表任意位置添加元素和在链表头添加元素,逻辑上有所不同。究其原因,是在链表添加过程中需要找到相应的前一个节点。因此,需要在链表中造一个虚拟头节点(dummy head)

代码实现

// 虚拟头节点
private Node dummyHead;
private int size;

public LinkedList() {
    dummyHead = new Node(null, null);
    size = 0;
}

// 在链表的index(0-based)位置添加新的元素e
// 在链表中不是一个常用的操作,通常练习用
public void add(int index, E e) {
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Add failed. Illegal index.");
    }
    Node prev = dummyHead;
     //i=0,prev指向0;i=1,prev指向1
    for (int i = 0; i < index; i++) {
        // 把当前prev存的这个节点的下一个节点放进prev这个变量中
        // prev这个变量在链表中会一直向前移动,直到移动到index-1这个位置
        // 最后就找到了待插入的那个节点的前一个节点
        prev = prev.next;
    }
    Node node = new Node(e);
    node.next = prev.next;
    prev.next = node;
    // 或者三面三行改成 perv.next= new Node(e, prev.next);
    size++;
}

// 在链表头添加新的元素e
public void addFirst(E e) {
    add(0, e)
}

// 在链表末尾添加新的元素e
public void addLast(E e) {
    add(size, e);
}

 6.5、链表的查询、更新与遍历

继续为我们的链表添加更多的操作。那么,首先是获得链表的第index个元素。
代码实现

// 获取在链表的index(0-based)位置的元素e
// 在链表中不是一个常用的操作,通常练习用
public E get(int index){
    if(index < 0 || index >= size){
        throw new IllegalArgumentException("Get failed. Illegal index.");
    }
    Node cur = dummyHead.next;
    for(int i = 0; i < index; i++){
        cur.next = cur;
    }
    return cur.e;
}

// 获得链表的第一个元素
public E getFirst(){
    return get(0);
}

// 获得链表的最后一个元素
public E getLast(){
    return get(size - 1);
}

// 修改在链表的index(0-based)位置的元素e
// 在链表中不是一个常用的操作,通常练习用
public void set(int index, E e){
    if(index < 0 || index >= size){
        throw new IllegalArgumentException("Update failed. Illegal index.");
    }
    Node cur = dummyHead.next;
    // 需要遍历到index节点
    for(int i = 0; i < index; i++){
        cur = cur.next;
    }
    // 再对index节点进行赋值
    cur.e = e;
}

// 查找链表是否含有元素e
public boolean contains(E e){
    Node cur = dummyHead.next;
    // 判断cur节点是否为空,就意味着cur节点为有效节点
    while (cur != null){
        if(cur.e.equals(e)){
            return true;
        }
        cur = cur.next;
    }
    return false;
}

 6.6、链表元素的删除

介绍了为链表添加元素,查询、更新元素,现在就插最后一个从链表中删除元素

  1. 要想删除索引为2的元素,需要找到索引为2的上一个节点prev,而prev.next便是待删除的节点(可称为delNode)
  2. 让prev.next指向delNode.next,即prev.next=delNode.next。也就是跳过了delNode节点
  3. 为了使得能够回收delNode节点的空间,因此需要将delNode.next置空,即delNode=null
  4. 这样一来,就完成了整个链表元素的删除操作

代码实现

// 从链表中删除index(0-based)位置的元素,返回删除的元素
// 在链表中不是一个常用的操作,通常仅供练习
public E remove(int index){
    if(index < 0 || index >= size){
        throw new IllegalArgumentException("Remove failed. Illegal index.");
    }
    Node prev = dummyHead;
    for(int i = 0; i < index; i++){
        prev = prev.next;
    }
    Node retNode = prev.next;
    prev.next = retNode.next;
    // 将retNode.next节点置空以便回收
    retNode.next = null;
    size--;
    return retNode.e;
}

// 从链表中删除第一个元素,返回删除的元素
public E removeFirst(){
    return remove(0);
}


// 从链表中删除最后一个元素,返回删除的元素
public E removeLast(){
    return remove(size - 1);
}

6.7、链表的时间复杂度分析

最后简单的分析一下,这个链表的时间复杂度。

  • 首先我们来看添加操作O(n)
    • 如果向链表尾添加一个元素addLast(e),则必须从链表头开始遍历每一个元素,因此是O(n)的时间复杂度。
    • 但是如果向链表头添加一个元素addFirst(e),它是O(1)的时间复杂度。
    • 如果在链表任意位置添加元素add(index,e),平均看是在链表中间插入元素即O(n/2)=O(n)的时间复杂度。
  • 对于删除操作来说是基本一样的分析过程O(n)
    • 如果想要删除最后一个元素,就需要链表头遍历一次,因此时间复杂度是O(n)
    • 删除第一个元素,需要O(1)的时间可以搞定了。
    • 但是如果想要删除任意位置节点的话,平均来看是O(n/2)~O(n)。
  • 由于链表不支持随机访问,所以要想修改某个位置的元素set(index, e),就必须从头遍历,所以这个修改操作的时间复杂度是O(n)
  • 对于查找操作 O(n)。
  • 所以要查询某个位置的元素set(index, e),就必须从头遍历,所以这个修改操作get(index)、contains(e)的时间复杂度是O(n)
  • 总体来说,链表的增、删、改、查的时间复杂度都是O(n)。如图所示:

总体来说,链表的增、删、改、查的时间复杂度都是O(n),比数组整体复杂度差。其中对于数组来说,如果有索引的话,可以快速访问,而链表没有这种操作。但是如果进对链表头进行操作,时间复杂度是O(1),对于只查链表头元素,时间复杂度是O(1),由于整体是动态的,不会大量浪费内存空间具有优势。

 6.8、链表的完整实现

  • 实现链表的业务逻辑如下:
public class LinkedList<E> {
    private class Node {
        public E e;
        public Node next;

        //构造函数
        public Node(E e, Node next) {
            this.e = e;
            this.next = next;
        }

        //只传了参数e的构造函数
        public Node(E e) {
            this(e, null);
        }

        //不传递参数的构造函数
        public Node() {
            this(null, null);
        }

        //方便打印测试
        @Override
        public String toString() {
            return e.toString();
        }
    }

    private Node dummyHead;
    private int size;

    //构造函数
    public LinkedList() {
        dummyHead = new Node(null, null);  //虚拟头节点
        size = 0;
    }

    //获取链表中的元素个数
    public int getSize() {
        return size;
    }

    //判断链表是否为空
    public boolean isEmpty() {
        return size == 0;
    }

    //在链表的index(0-based)位置添加新的元素e
    //在链表中不是一个常用操作
    public void add(int index, E e) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed. Illegal index.");
        }

        Node prev = dummyHead;
        for (int i = 0; i < index; i++) {
            prev = prev.next;
        }

        prev.next = new Node(e, prev.next);
        size++;
    }

    //在链表头添加新元素e
    public void addFirst(E e) {
        add(0, e);
    }

    //在链表末尾添加新的元素e
    public void addLast(E e) {
        add(size, e);
    }

    //获取链表的index(0-based)位置的元素
    //在链表中也不是一个常用操作
    public E get(int index) {
        if (index < 0 || index > size - 1) {
            throw new IllegalArgumentException("Get failed. Illegal index.");
        }

        Node cur = dummyHead.next;
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        return cur.e;
    }

    //获得链表的第一个元素
    public E getFirst() {
        return get(0);
    }

    //获得链表的最后一个元素
    public E getLast() {
        return get(size - 1);
    }

    //修改链表的index(0-based)位置的元素为e
    //在链表中也不是一个常用操作
    public void set(int index, E e) {
        if (index < 0 || index > size - 1) {
            throw new IllegalArgumentException("Set failed. Illegal index.");
        }

        Node cur = dummyHead.next;
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        cur.e = e;
    }

    //查找链表中是否存在元素e
    public boolean contains(E e) {
        Node cur = dummyHead.next;
        while (cur != null) {
            if (cur.e.equals(e)) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

    //删除链表的index(0-based)位置的元素,并返回该元素
    //在链表中也不是一个常用操作
    public E remove(int index) {
        if (index < 0 || index > size - 1) {
            throw new IllegalArgumentException("Remove failed. Illegal index.");
        }
        Node prev = dummyHead;
        for (int i = 0; i < index; i++) {
            prev = prev.next;
        }
        Node retNode = prev.next;
        prev.next = retNode.next;
        retNode.next = null;
        size--;

        return retNode.e;
    }

    //删除链表中的第一个元素,并返回该元素
    public E removeFirst() {
        return remove(0);
    }

    //删除链表中的最后一个元素,并返回该元素
    public E removeLast() {
        return remove(size - 1);
    }

    // 从链表中删除元素e
    public void removeElement(E e){

        Node prev = dummyHead;
        while(prev.next != null){
            if(prev.next.e.equals(e))
                break;
            prev = prev.next;
        }

        if(prev.next != null){
            Node delNode = prev.next;
            prev.next = delNode.next;
            delNode.next = null;
            size --;
        }
    }


    //方便打印测试
    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
//        Node cur = dummyHead.next;
//        while (cur != null) {
//            res.append(cur + "->");
//            cur = cur.next;
//        }
        for (Node cur = dummyHead.next; cur != null; cur = cur.next) {
            res.append(cur + "->");
        }
        res.append("NULL");

        return res.toString();
    }
}
  • 测试的业务逻辑如下:

  • public class Main {
    
        public static void main(String[] args) {
            LinkedList<Integer> linkedList = new LinkedList<>();
            for (int i = 0; i < 5; i++) {
                linkedList.addFirst(i);
                System.out.println(linkedList);
            }
    
            linkedList.add(2, 666);
            System.out.println(linkedList);
    
            linkedList.remove(2);
            System.out.println(linkedList);
    
            linkedList.removeFirst();
            System.out.println(linkedList);
    
            linkedList.removeLast();
            System.out.println(linkedList);
        }
    }
  • 输出结果:
  • 0->NULL
    1->0->NULL
    2->1->0->NULL
    3->2->1->0->NULL
    4->3->2->1->0->NULL
    4->3->666->2->1->0->NULL
    4->3->2->1->0->NULL
    3->2->1->0->NULL
    3->2->1->NULL

7、 使用链表来实现一个"栈"

如果我们只对链表的头进行添加和删除操作,那么时间复杂度是O(1),如果我们只查链表头的元素,那么时间复杂度也是O(1),满足这些条件的数据结构,我们很容易就会想到"栈",对于"栈"而言,遵循后进先出的原则,只对栈的一端,也就是"栈顶"进行操作,无论是添加、删除还是查看元素,都在栈顶进行。所以,我们就可以把链表头当作栈顶,用链表来作为栈的底层实现,来实现出一个栈。

  • 链表栈的实现及测试的业务逻辑如下:
public class LinkedListStack<E> implements Stack<E> {

    private LinkedList<E> list;

    //构造函数
    public LinkedListStack() {
        list = new LinkedList<>();
    }

    //实现getSize方法
    @Override
    public int getSize() {
        return list.getSize();
    }

    //实现isEmpty方法
    @Override
    public boolean isEmpty() {
        return list.isEmpty();
    }

    //实现push方法
    @Override
    public void push(E e) {
        list.addFirst(e);
    }

    //实现pop方法
    @Override
    public E pop() {
        return list.removeFirst();
    }

    //实现peek方法
    @Override
    public E peek() {
        return list.getFirst();
    }

    //方便打印测试
    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        res.append("Stack: top ");
        res.append(list);
        return res.toString();
    }

    //测试
    public static void main(String[] args) {

        LinkedListStack<Integer> stack = new LinkedListStack<>();
        //测试入栈push
        for (int i = 0; i < 5; i++) {
            stack.push(i);
            System.out.println(stack);
        }
        //测试出栈
        stack.pop();
        System.out.println(stack);
    }
}
  • 输出结果:
  • Stack: top 0->NULL
    Stack: top 1->0->NULL
    Stack: top 2->1->0->NULL
    Stack: top 3->2->1->0->NULL
    Stack: top 4->3->2->1->0->NULL
    Stack: top 3->2->1->0->NULL

8、数组栈与链表栈的性能比较

  • 测试的业务逻辑如下:
  • import java.util.Random;
    
    public class Main {
    
        //测试使用stack运行opCount个push和pop操作所需的时间,单位:秒
        private static double testStack(Stack<Integer> stack, int opCount) {
    
            long startTime = System.nanoTime();
    
            Random random = new Random();
            for (int i = 0; i < opCount; i++) {
                stack.push(random.nextInt(Integer.MAX_VALUE));
            }
            for (int i = 0; i < opCount; i++) {
                stack.pop();
            }
    
            long endTime = System.nanoTime();
            return (endTime - startTime) / 1000000000.0;
        }
    
        public static void main(String[] args){
            int opCount = 10000;
    
            ArrayStack<Integer> arrayStack = new ArrayStack<>();
            double time1 = testStack(arrayStack,opCount);
            System.out.println("ArrayStack, time: " + time1 + " s");
    
            LinkedListStack<Integer> linkedListStack = new LinkedListStack<>();
            double time2 = testStack(linkedListStack,opCount);
            System.out.println("LinkedListStack, time: " + time2 + " s");
    
            // 这二者的时间比较很复杂,ArrayStack会在扩容和缩容操作上面耗费时间,LinkedListStack则会在创建新的Node上面耗费时间
        }
  • 这两种栈的时间复杂度基本处于相同的水平

9、使用链表实现一个"队列"

  • 针对链表,添加一个尾指针tail,tail端添加元素容易,删除元素不容易(需要找到待删除元素的前一个元素),而在head添加和删除一个元素都比较容易。
  • 故使用tail端插入元素,作为队尾,在head端删除元素,作为队首。此时我们不使用虚拟头结点,因为不涉及对链表中间元素的插入和删除,只针对对首和队尾插入和删除。故不需要统一。
  • 由于没有dummyNode,当队列为空时,head和tail都指向空节点。
  • 链表队列的实现及测试的业务逻辑如下
  • public class LinkListQueue<E> implements Queue<E> {
    
        private class Node {
            public E e;
            public Node next;
    
            public Node(E e, Node next) {
                this.e = e;
                this.next = next;
            }
    
            public Node(E e) {
                this(e, null);
            }
    
            public Node() {
                this(null, null);
            }
    
            @Override
            public String toString() {
                return e.toString();
            }
        }
    
        private Node head, tail;
        private int size;
    
        public LinkListQueue() {
            head = null;
            tail = null;
            size = 0;
        }
    
        //实现getSize
        @Override
        public int getSize() {
            return size;
        }
    
        //实现isEmpty
        @Override
        public boolean isEmpty() {
            return size == 0;
        }
    
        //实现enqueue
        @Override
        public void enqueue(E e) {
            if (tail == null) {
                tail = new Node(e);
                head = tail;
            } else {
                tail.next = new Node(e);
                tail = tail.next;
            }
            size++;
        }
    
        //实现dequeue
        @Override
        public E dequeue() {
            if (isEmpty()) {
                throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
            }
            Node retNode = head;
            head = head.next;
            retNode.next = null;
            if (head == null) {
                tail = null;
            }
            size--;
            return retNode.e;
        }
    
        //实现getFront
        public E getFront() {
            if (isEmpty()) {
                throw new IllegalArgumentException("Queue is empty.");
            }
            return head.e;
        }
    
        //方便打印测试
        public String toString() {
            StringBuilder res = new StringBuilder();
            res.append("Queue: front ");
    
            Node cur = head;
            while (cur != null) {
                res.append(cur + "->");
                cur = cur.next;
            }
            res.append("NULL");
            return res.toString();
        }
    
        //测试
        public static void main(String[] args) {
            LinkListQueue<Integer> queue = new LinkListQueue<>();
            for (int i = 0; i < 6; i++) {
                queue.enqueue(i);
                System.out.println(queue);
    
                if (i % 3 == 2) {
                    queue.dequeue();
                    System.out.println(queue);
                }
            }
        }
  • 输出结果:
  • Queue: front 0->NULL
    Queue: front 0->1->NULL
    Queue: front 0->1->2->NULL
    Queue: front 1->2->NULL
    Queue: front 1->2->3->NULL
    Queue: front 1->2->3->4->NULL
    Queue: front 1->2->3->4->5->NULL
    Queue: front 2->3->4->5->NUL

    10、 数组队列、循环队列和链表队列的性能比较

  • 测试的业务逻辑如下:
  • import java.util.Random;
    
    public class Main {
    
        // 测试使用q运行opCount个enqueue和dequeue操作所需要的时间,单位:秒
        private static double testQueue(Queue<Integer> q, int opCount) {
            long startTime = System.nanoTime();
    
            Random random = new Random();
            for (int i = 0; i < opCount; i++) {
                q.enqueue(random.nextInt(Integer.MAX_VALUE));
            }
            for (int i = 0; i < opCount; i++) {
                q.dequeue();
            }
    
            long endTime = System.nanoTime();
            return (endTime - startTime) / 1000000000.0;
        }
    
        public static void main(String[] args) {
    
            int opCount = 100000;
    
            ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
            double time1 = testQueue(arrayQueue, opCount);
            System.out.println("ArrayQueue, time: " + time1 + " s");
    
            LoopQueue<Integer> loopQueue = new LoopQueue<>();
            double time2 = testQueue(loopQueue, opCount);
            System.out.println("LoopQueue, time: " + time2 + " s");
    
            LinkListQueue<Integer> linkListQueue = new LinkListQueue<>();
            double time3 = testQueue(linkListQueue, opCount);
            System.out.println("LinkListQueue, time: " + time3 + " s");
    
        }
    }

    输出结果:

  • ArrayQueue, time: 3.069366801 s
    LoopQueue, time: 0.010702659 s
    LinkListQueue, time: 0.007079073 s
    

11、小练习,删除掉链表中所有值为val的节点

  • 不使用dummyHead的实现方法
  • class Solution {
        public ListNode removeElements(ListNode head, int val) {
    
            while (head != null && head.val == val) {
                //head = head.next;
                ListNode delNode = head;
                head = head.next;
                delNode.next = null;
            }
    
            if (head == null) {
                return null;
            }
    
            ListNode prev = head;
            while (prev.next != null) {
                if (prev.next.val == val) {
                    //prev.next = prev.next.next;
                    ListNode delNode = prev.next;
                    prev.next = delNode.next;
                    delNode.next = null;
                } else {
                    prev = prev.next;
                }
            }
    
            return head;
        }
    }
    
  • 使用dummyHead的实现方法:
  • class Solution2 {
        public ListNode removeElements(ListNode head, int val) {
    
            ListNode dummyHead = new ListNode(-1);
            dummyHead.next = head;
    
            ListNode prev = dummyHead;
            while (prev.next != null) {
                if (prev.next.val == val) {
                    //prev.next = prev.next.next;
                    ListNode delNode = prev.next;
                    prev.next = delNode.next;
                    delNode.next = null;
                } else {
                    prev = prev.next;
                }
            }
    
            return dummyHead.next;
        }
    }
    
  • 使用dummyHead之后,代码变得更加简洁

12、递归

  • 从本质上讲,递归,就是将原来的问题转化为更小的同一个问题;

  • 递归举例,数组求和:

  • Sum(arr[0...n-1]) = arr[0] + Sum(arr[1...n-1])   <-- 转化为更小的同一个问题
    Sum(arr[1...n-1]) = arr[1] + Sum(arr[2...n-1])   <-- 转化为更小的同一个问题
               ......
    Sum(arr[n-1...n-1]) = arr[n-1] + Sum([])   <-- 最基本的问题
    
  • 代码简单实现:
  • public class Sum {
        public static int sum(int[] arr) {
            return sum(arr, 0);
        }
    
        //计算arr[l...n)这个区间内所有数的和
        private static int sum(int[] arr, int l) {
            if (l == arr.length) {        //  <--  求解最基本问题
                return 0;
            }
            return arr[l] + sum(arr, l + 1);   //  <-- 将原问题简化为更小的问题
        }
    
        //测试
        public static void main(String[] args) {
            int[] arr = {1, 2, 3, 4, 5, 6, 7, 8};
            System.out.println(sum(arr));
        }
    }
    

13、 链表的天然递归性

  • 通过下图,很容易理解为什么链表具有天然的递归性

  •  

  • 用递归的思想解决删除链表中的节点的问题,原理示意图:
  •  

  • 用递归实现删除链表中所有包含指定元素的节点的业务逻辑:
  • class Solution3 {
    
        public ListNode removeElements(ListNode head, int val) {
            if (head == null) {
                return null;
            }
    
            head.next = removeElements(head.next, val);
    
            //return head.val == val ? head.next : head;
            if (head.val == val) {
                return head.next;
            } else {
                return head;
            }
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值