数组和链表 (From Day 2 & Day 3)

1.数组

1)数组的三个问题
  1. 数组我们都很熟悉,那你理解的数组是什么样的呢?它的最主要特点是什么呢?

A1:数组的本质是固定大小的连续的内存空间,并且这片连续的内存空间又被分割成等长的小空间。
   它最主要的特点是随机访问。
   
   数组的缺点:

   1.数组的长度是固定的
   2.数组只能存储同一种数据类型的元素

注意:在Java中只有一维数组的内存空间是连续,多维数组的内存空间不一定连续。

那么数组又是如何实现随机访问的呢?

通过寻址公式:i_address = base_address + i * type_length
  1. 为什么数组的索引是一般都是从0开始的呢?

假设索引不是从0开始的,而是从1开始的,那么我们有两种处理方式:

1.寻址公式变为: i_address = base_address + (i – 1) * type_length

2.浪费开头的一个内存空间,寻址公式不变。

在计算机发展的初期,不管是CPU资源,还是内存资源都极为宝贵,
所以在设计编程语言的时候,索引就从0开始了,而我们也一直延续了下来。
  1. 为什么数组的效率比链表高?

CPU、内存和IO设备,它们传输数据的速率是存在很大差异的,怎么个差异呢?
     举个例子,CPU一天,内存一年;内存一天,IO十年。

根据木桶理论:木桶能装多少水,取决于最短的那块木板。
那么程序的性能主要取决于IO设备的性能?也就是说,我们提升CPU和内存的传输速率收效甚微。

实际是这样的吗?当然不是!那我们是怎么解决它们之间的速率差异的呢?

1.CPU 和 内存
      高速缓存
      编译器的指令重排序

2.内存和 IO
      缓存:将磁盘上的数据缓存在内存。

3.CPU 和 IO
      中断技术

数组可以更好地利用CPU的高速缓存!
2)数组的效率和特点
数组的基本操作

1.添加 (保证元素的顺序)
      最好情况:O(1)

      最坏情况:移动n个元素,O(n)

      平均情况:移动 n/2 个元素,O(n)

2.删除 (保证元素的顺序)
     最好情况:O(1)

     最坏情况:移动n-1个元素,O(n)

     平均情况:移动(n-1)/2个元素,O(n)


3.查找
     a. 根据索引查找元素:O(1)

     b. 查找数组中与特定值相等的元素

           1.大小无序:O(n)

           2.大小有序:O(log2n)    

总结: 数组增删慢,查找快。

数组的基本操作

2.链表

1)概念

在这里插入图片描述

2)分类

在这里插入图片描述

3)单链表和双向链表的特点和效率
  1. 单链表
 循环链表我们用的一般比较少,但是当处理的数据具有环形结构时,就特别适合用循环链表,
 比如约瑟夫问题。接下来我们讨论下单链表和双向链表。

单链表:

1.增加(在某个结点后面添加)

2.删除(在某个结点后面删除)

3.查找
      a. 根据索引查找元素           

      b. 查找链表中与特定值相等的元素

           1.元素大小有序              

           2.元素大小无序

总结:链表增删快,查找慢。

在这里插入图片描述

  1. 双向链表
双向链表:

很容易验证,前面那些操作,双向链表和单链表的时间复杂度是一样的。
那为什么在工程上,我们用的一般是双向链表而不是单链表呢 
(比如JDK中的 LinkedList & LinkedHashMap)?

那自然是双向链表有单链表没有的独特魅力——它有一条指向前驱结点的链接。

1.增加 (在某个结点前面添加元素)
2.删除 (删除该结点)

3.查找
      a. 查找前驱结点

      b. 根据索引查找元素

      c. 查找链表中与特定值相等的元素

          1. 元素大小无序

          2. 元素大小有序

总结:虽然双向链表更占用内存空间,但是它在某些操作上的性能是优于单链表的。

思想:用空间换取时间。

在这里插入图片描述

4)缓存
缓存就是一种用空间换取时间的技术。

内存大小是有限的,所以缓存不能无限大。那么当缓存满的时候,再向缓存中添加数据,该怎么办呢?

缓存淘汰策略:

         1 FIFO (First In First Out)

         2 LFU (Least Frequently Used)

         3 LRU (Least Recently Used)

LRU算法中我们就用到了链表!

1.添加 (认为尾节点是最近最少使用的数据)
      a. 如果缓存中已经存在该数据

          删除该结点,添加到头结点

      b. 如果缓存中不存在该数据

          1. 缓存没满

                添加到头结点

          2. 缓存满了

                删除尾节点, 在头结点添加    

用链表实现LRU算法:
在这里插入图片描述

5)链表的3个练习题
  1. 求单链表的中间元素
/*
求单链表的中间元素
示例1:
输入:1 --> 2 --> 3
输出: 2
示例2:
输入:1 --> 2 --> 3 --> 4
输出:2

思路:
    如果是数组,我们可以怎么求中间元素. arr[(arr.length - 1)/2]
    a. 求链表的长度
    b. 从头开始遍历链表,并计数
 */
public class Ex1 {
    public static int middleElement(Node head) {
        // 求链表的长度
        int length = 0;
        Node x = head;
        while (x != null) {
            length++;
            x = x.next;
        }
        // 计算中间元素的索引
        int index = (length - 1) / 2;
        // 从头开始遍历链表,并计数
        int i = 0;
        x = head;
        while (i < index) {
            x = x.next;
            i++;
        }
        return x.value;
    }

    public static void main(String[] args) {
        // 1 --> 2 --> 3
        /*Node head = new Node(3);
        head = new Node(2, head);
        head = new Node(1, head);
        System.out.println(middleElement(head));*/

        // 1 --> 2 --> 3 --> 4
        Node head = new Node(4);
        head = new Node(3, head);
        head = new Node(2, head);
        head = new Node(1, head);
        System.out.println(middleElement(head));
    }
}

  1. 判断单链表中是否有环
/*
2. 判断单链表中是否有环
思路1:一刀切
    给定一个阈值100ms
    如果程序运行时间超过10ms, 有环, 否则无环。
思路2:迷雾森林
    将经过的结点做标记。Collection visited.
    遍历链表:
        判断当前结点是否在visited集合中存在。contains
        存在:返回true。
        不存在:遍历下一个结点。
    遍历结束:返回false。
思路3:跑道(快慢指针)
    1. 快慢指针都指向头结点,慢指针每次走一步, 快指针每次走两步。
    2. 如果快指针走到终点,说明无环
    3. 否则快慢指针一定会再次相遇, 说明有环。
 */
public class Ex2 {
    /*
    时间复杂度:O(n^2) --> O(n)
    空间复杂度:O(n)
     */
   /* public static boolean hasCircle(Node head) {
        // Collection visited = new ArrayList(); // HashSet的查找时间复杂度为O(1)
        Collection visited = new HashSet();
        Node x = head;
        while (x != null) {
            if (visited.contains(x)) return true;
            // 做标记
            visited.add(x);
            x = x.next;
        }
        return false;
    }*/

   /*
   时间复杂度:假设环外的结点有a个,环内的结点有r个
     最好情况:O(a)
     最坏情况:O(a + r)
     平均情况:O(a + r/2)
   空间复杂度:O(1)
    */
    public static boolean hasCircle(Node head) {
        Node slow = head;
        Node fast = head;
        do {
            // 判断快指针是否走到了终点 (短路原则)
            if (fast == null || fast.next == null) return false;
            slow = slow.next;
            fast = fast.next.next;
        } while (slow != fast);
        return true;
    }

    public static void main(String[] args) {
        // 1 --> 2 --> 3 --> 4
        /*Node head = new Node(4);
        head = new Node(3, head);
        head = new Node(2, head);
        head = new Node(1, head);
        System.out.println(hasCircle(head));*/

        // 1 --> 2 --> 3 --> 4 --> 2...
        /*Node node = new Node(4);
        Node head = new Node(3, node);
        head = new Node(2, head);
        node.next = head;
        head = new Node(1, head);
        System.out.println(hasCircle(head));*/

        // 1 --> 2 --> 3 --> 4 --> 4 ...
        Node head = new Node(4);
        head.next = head;
        head = new Node(3, head);
        head = new Node(2, head);
        head = new Node(1, head);
        System.out.println(hasCircle(head));
    }
}

画图说明:
在这里插入图片描述

  1. 扩展:判断单链表中是否有环,如果有环,返回入环的第一个结点
/*
3. 判断单链表中是否有环
   如果有环,返回入环的第一个结点
   否则返回null

思路1: 迷雾森林
    将经过的结点做标记。Collection visited.
    遍历链表:
        判断当前结点是否在visited集合中存在。contains
        存在:返回该节点
        不存在:遍历下一个结点。
    遍历结束:返回null。
思路2:跑道(快慢指针)

 */
public class Ex3 {
    /*
    时间复杂度:O(n)
    空间复杂度:O(n)
     */
    /*public static Node hasCircle(Node head) {
        Collection visited = new HashSet();
        Node x = head;
        while (x != null) {
            if (visited.contains(x)) return x;
            visited.add(x);
            x = x.next;
        }
        return null;
    }*/

    /*
    时间复杂度:假设环外的结点有a个,环内的结点有r个
      最好情况:O(2a)
      最坏情况:O(2a + r)
      平均情况:O(2a + r/2)
    空间复杂度:O(1)
     */
    public static Node hasCircle(Node head) {
        Node fast = head;
        Node slow = head;
        do {
            if (fast == null || fast.next == null) return null;
            slow = slow.next;
            fast = fast.next.next;
        } while (slow != fast);
        // 将fast移动到头结点
        fast = head;
        while (slow != fast) {
            fast = fast.next;
            slow = slow.next;
        }
        return fast;
    }

    public static void main(String[] args) {
        // 1 --> 2 --> 3 --> 4
        /*Node head = new Node(4);
        head = new Node(3, head);
        head = new Node(2, head);
        head = new Node(1, head);
        System.out.println(hasCircle(head));*/

        // 1 --> 2 --> 3 --> 4 --> 2...
       /* Node node = new Node(4);
        Node head = new Node(3, node);
        head = new Node(2, head);
        node.next = head;
        head = new Node(1, head);
        System.out.println(hasCircle(head));*/

        // 1 --> 2 --> 3 --> 4 --> 4 ...
        /*Node head = new Node(4);
        head.next = head;
        head = new Node(3, head);
        head = new Node(2, head);
        head = new Node(1, head);
        System.out.println(hasCircle(head));*/
    }
}

画图说明:
在这里插入图片描述

  1. 反转单链表
/*
反转单链表
示例
输入:1 --> 2 --> 3 --> null
输出:3 --> 2 --> 1 --> null

思路1:头插法
思路2:递归
 */
public class Ex1 {
    /*public static Node reverse(Node head) {
        Node prev = null;
        Node curr = head;
        while (curr != null) {
            Node next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }*/

    public static Node reverse(Node head) {
        if (head.next == null) return head;
        // 反转head.next
        Node reversed = reverse(head.next);
        // 反转head结点
        head.next.next = head;
        head.next = null;
        return reversed;
    }
    public static void print(Node head) {
        Node x = head;
        while (x != null) {
            System.out.print(x.value);
            if (x.next != null) {
                System.out.print(" --> ");
            }
            x = x.next;
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Node head = new Node(4);
        head = new Node(3, head);
        head = new Node(2, head);
        head = new Node(1, head);
        print(head);
        head = reverse(head);
        print(head);
    }
}

头插法 画图说明:
在这里插入图片描述
递归法 画图说明:在这里插入图片描述

3.数组和链表的区别

在这里插入图片描述


总结起来就是:数组更加高效,链表更加灵活。

而且数组和链表是实现其他数据结构的基础,比如栈、队列、树、哈希表、图等


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值