1.数组
1)数组的三个问题
- 数组我们都很熟悉,那你理解的数组是什么样的呢?它的最主要特点是什么呢?
A1:数组的本质是固定大小的连续的内存空间,并且这片连续的内存空间又被分割成等长的小空间。
它最主要的特点是随机访问。
数组的缺点:
1.数组的长度是固定的
2.数组只能存储同一种数据类型的元素
注意:在Java中只有一维数组的内存空间是连续,多维数组的内存空间不一定连续。
那么数组又是如何实现随机访问的呢?
通过寻址公式:i_address = base_address + i * type_length
- 为什么数组的索引是一般都是从0开始的呢?
假设索引不是从0开始的,而是从1开始的,那么我们有两种处理方式:
1.寻址公式变为: i_address = base_address + (i – 1) * type_length
2.浪费开头的一个内存空间,寻址公式不变。
在计算机发展的初期,不管是CPU资源,还是内存资源都极为宝贵,
所以在设计编程语言的时候,索引就从0开始了,而我们也一直延续了下来。
- 为什么数组的效率比链表高?
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.增加(在某个结点后面添加)
2.删除(在某个结点后面删除)
3.查找
a. 根据索引查找元素
b. 查找链表中与特定值相等的元素
1.元素大小有序
2.元素大小无序
总结:链表增删快,查找慢。
- 双向链表
双向链表:
很容易验证,前面那些操作,双向链表和单链表的时间复杂度是一样的。
那为什么在工程上,我们用的一般是双向链表而不是单链表呢
(比如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 --> 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));
}
}
- 判断单链表中是否有环
/*
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));
}
}
画图说明:
- 扩展:判断单链表中是否有环,如果有环,返回入环的第一个结点
/*
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 --> 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.数组和链表的区别
总结起来就是:数组更加高效,链表更加灵活。
而且数组和链表是实现其他数据结构的基础,比如栈、队列、树、哈希表、图等