重要理论
操作的速度,并不按时间计算,而是按步数计算。如果A操作要5步,B操作要500步,那么我们可以很肯定地说,无论是在什么样的硬件上对比,A都快过B。因此,衡量步数是分析速度的关键。操作的速度,也常被称为时间复杂度。在本书中,我们会提到速度、时间复杂度、效率、性能,但它们其实指的都是步数。****
对于复杂度计算,不仅要考虑最坏情况,还要考虑平均情况。所谓平均情况,就是那些最常遇见的情况。最坏情况和最好情况都是不常见的。
复杂度 O(N)
时间复杂度
大O记法不只是用固定的数字(如22、440)来表示算法的步数,而是基于要处理的数据量来描述算法所需的步数。或者说,大O解答的是这样的问题:当数据增长时,步数如何变化。
O(N)算法所需的步数等于数据量,意思是当数组增加一个元素时,O(N)算法就要增加1步。而O(1)算法无论面对多大的数组,其步数都不变。
从图中可以看出,O(N)呈现为一条对角线。当数据增加一个单位时,算法也随之增加一步。也就是说,数据越多,算法所需的步数就越多。O(N)也被称为线性时间。
相比之下,O(1)则为一条水平线,因为不管数据量是多少,算法的步数都恒定。所以,O(1)也被称为常数时间。因为大O主要关注的是数据量变动时算法的性能变化,所以你会发现,即使一个算法的恒定步数不是1,它也可以被归类为O(1)。
空间复杂度
空间复杂度的度量方式,去估计它们会消耗多少内存。
当内存有限时,空间复杂度便会成为选择算法的一个重要的参考因素。比如说,在给小内存的小型设备写程序时,或是处理一些会迅速占满大内存的大数据时都会考虑空间复杂度。
计算机科学家还是用描述时间复杂度的大O记法来描述空间复杂度。当所处理的数据有N个元素时,该算法还需额外消耗多少元素大小的内存空间。
时间复杂度的O(1)意味着一个算法无论处理多少数据,其速度恒定。相似地,空间复杂度的O(1)则意味着一个算法无论处理多少数据,其消耗的内存恒定。
数据结构
一般数据结构都有以下4种操作:读取、查找、插入、删除
数组
(1) 计算机可以一步就跳到任意一个内存地址上。(就好比,要是你知道大街123号在哪儿,那么就可以直奔过去。)
(2) 数组本身会记有第一个格子的内存地址,因此,计算机知道这个数组的开头在哪里。
(3) 数组的索引从0算起。
数组查询时需要从头遍历查询到目标数据位置,因此最多需要N步;数组插入时需要在将目标位置右侧的数据依次右移,所以最多需要N+1步;删除时需要先将目标位置数据删除,再将右侧的数据左移,因此最多需要N步。
链表
散列表
大多数编程语言都自带散列表这种能够快速读取的数据结构。但在不同的语言中,它有不同的名字,除了散列表,还有散列、映射、散列映射、字典、关联数组。散列表由一对对的数据组成。一对数据里,一个叫作键,另一个叫作值。键和值应该具有某种意义上的关系。(java 的 Map)
归根到底,散列表的效率取决于以下因素:
1)要存多少数据。
2)有多少可用的格子
3)用什么样的散列函数
使用散列表时所需要权衡的:既要避免冲突,又要节约空间。要想解决这个问题,可参考计算机科学家研究出的黄金法则:每增加7个元素,就增加10个格子。数据量与格子数的比值称为负载因子。把这个术语代入刚才的理论,就是:理想的负载因子是0.7(7个元素 / 10个格子)。
栈
栈存储数据的方式跟数组一样,都是将元素排成一行。只不过它还有以下3条约束。
1)只能在末尾插入数据。
2)只能读取末尾的数据。
3)只能移除末尾的数据。
绝大部分计算机科学家都把栈的末尾称为栈顶,把栈的开头称为栈底。
压栈和出栈可被形容为LIFO(last in,first out)后进先出解释起来就是最后入栈的元素,会最先出栈。就像无心向学的学生,最迟到校的总是他,最早回家的也是他。
Ruby的数组自带push和pop方法,是在数组结尾插入和删除元素的便捷调用。只使用这两个方法的话,数组便形同于栈。
`以下是Java实现栈的示例代码:
public class Stack {
private int[] data;
private int maxSize;
private int top;
public Stack(int maxSize) {
this.maxSize = maxSize;
this.data = new int[maxSize];
this.top = -1;
}
public void push(int value) {
if (isFull()) {
throw new RuntimeException("Stack is full");
}
data[++top] = value;
}
public int pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return data[top--];
}
public int peek() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return data[top];
}
public boolean isEmpty() {
return top == -1;
}
public boolean isFull() {
return top == maxSize - 1;
}
}`
栈是一种先进后出(LIFO,Last In First Out)的数据结构,栈的基本操作包括push、pop、peek、isEmpty和isFull。在实现中,我们使用一个数组来存储栈内元素,以及一个top变量来表示栈顶元素的位置。其中,push()方法实现元素的进栈操作,pop()方法实现元素的出栈操作,peek()方法返回栈顶元素但不弹出,isEmpty()方法检查栈是否为空,isFull()方法检查栈是否已满。