数据结构与算法:如何在项目当中使用数据结构与算法?它是如何解决我们的一些问题的?
一、目录
- 我们为什么要使用数据结构和算法?
- 举例:数据结构和算法如何来优化性能呢?
- 时间复杂度和空间复杂度如何理解?
- 我们如何知道什么时候应该使用数据结构与算法?应该是什么数据结构吗?
二、我们为什么要使用数据结构和算法?
想象一下,你在开发一个网站的用户系统。
这个用户系统的功能之一是,对某个尝试登录用户的ID去核实是否合法,这就需要去存储着海量数据的数据库中查找这个ID。假设这个尝试登录用户的ID是guest,一个可行的办法是,对数据库中的每个记录去匹配是否与guest一致。
然而,效率更高的方法是,预先对数据库中所有的数据按照字母顺序进行排序,接着就可以从有序数据的中间开始查找,去通过二分查找不断缩小查找范围。如果这个系统的注册用户只有不足16个,两种查找方式所花费时间的差异也许并不明显,无非就是16次匹配与log₂16 = 4次匹配的区别。但如果注册用户的数量达到了1000万,两种查找算法的效率可能就是1000万次和24次的区别了(log₂10000000 = 23.25)。
这就是数据结构和算法的作用。
数据结构和算法是相辅相成的。数据结构为算法提供了基础,使得算法能够高效地处理数据。而算法则是对数据结构的操作,通过算法可以实现数据的存储、检索、更新和删除等操作。在实际应用中,选择适当的数据结构和算法可以显著提高程序的性能和可靠性。
三、举例:数据结构和算法如何来优化性能呢?
下面,我们就举部分代码的例子来看看数据结构的基础应用。
3.1 举例一:数据查询
场景:在移动端应用中,用户需要快速查找联系人。
原始方法:使用线性搜索遍历联系人列表(O(n)复杂度)。
优化方法:使用哈希表(如Java中的HashMap)存储联系人,实现O(1)平均查找复杂度。
优化前(线性搜索):
import java.util.ArrayList;
import java.util.List;
public class ContactList {
private List<Contact> contacts;
public ContactList() {
contacts = new ArrayList<>();
}
public void addContact(Contact contact) {
contacts.add(contact);
}
public Contact findContactByName(String name) {
for (Contact contact : contacts) {
if (contact.getName().equals(name)) {
return contact;
}
}
return null;
}
// Contact类定义和其他方法...
}
优化后(使用哈希表):
import java.util.HashMap;
import java.util.Map;
public class ContactList {
private Map<String, Contact> contactMap;
public ContactList() {
contactMap = new HashMap<>();
}
public void addContact(Contact contact) {
contactMap.put(contact.getName(), contact);
}
public Contact findContactByName(String name) {
return contactMap.get(name); // O(1)复杂度
}
}
通过花时间,去循环遍历,改为了花空间,去添加key,方便找到值。
Map底层不是for循环?使用 HashMap 的 get 方法就像知道你要找的书的确切位置(比如你知道它在书架的哪一层、哪一格)。你可以直接走到那个位置并拿起那本书,而不需要检查书架上的每一本书。使用 for 循环遍历查找就像你不知道你要找的书在哪里,所以你从书架的一头开始,一本一本地检查,直到找到你要找的那本书或检查完所有书为止。
举例二:增删数据
场景:在移动端应用中,需要频繁地向数组中添加和删除元素。
原始方法:使用原生数组,每次添加或删除元素时都可能需要复制整个数组(O(n)复杂度)。
优化方法:使用ArrayList,它基于动态数组实现,可以自动调整大小,并且提供了O(1)时间复杂度的随机访问和O(amortized)时间复杂度的添加/删除操作。
优化前(使用原生数组):
// 假设有一个固定大小的数组
int[] array = new int[10];
int index = 0;
// 添加元素(需要手动管理数组大小和索引)
if (index < array.length) {
array[index++] = newValue;
} else {
// 数组已满,需要复制并扩展数组大小(这里省略了具体实现)
// ...
}
优化后(使用ArrayList):
import java.util.ArrayList;
import java.util.List;
public class DynamicArray {
private List<Integer> array;
public DynamicArray() {
array = new ArrayList<>();
}
public void addElement(int value) {
array.add(value); // O(amortized)时间复杂度
}
public int getElement(int index) {
return array.get(index); // O(1)时间复杂度
}
// 其他方法,如删除元素等...
}
四、时间复杂度和空间复杂度
4.1 时间复杂度
时间复杂度衡量的是算法执行所需的时间与输入数据规模之间的关系。它告诉我们,当输入数据变得更大时,算法的运行时间会怎样变化。
-
O(1) 常数时间复杂度:无论输入数据有多大,算法的执行时间都是固定的。比如,访问数组中的某个元素,无论数组有多大,访问时间都是一样的。
例子:int value = array[5]; // 访问数组第6个元素,时间复杂度为O(1)。
-
O(n) 线性时间复杂度:算法的执行时间与输入数据的规模成正比。比如,遍历一个数组中的每个元素。
例子:for (int i = 0; i < array.length; i++) { … } // 遍历数组,时间复杂度为O(n)。’
-
O(n^2) 平方时间复杂度:算法的执行时间与输入数据规模的平方成正比。比如,嵌套循环遍历二维数组。
例子:for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { … } } // 嵌套循环,时间复杂度为O(n^2)。
-
O(log n) 对数时间复杂度:算法的执行时间随着输入数据规模的增加而对数级增长。这通常出现在分治算法中,如二分查找。
例子:在有序数组中查找元素,每次将搜索范围减半,直到找到元素或搜索范围为空。
4.2 空间复杂度
空间复杂度衡量的是算法在运行过程中所占用的存储空间大小。它告诉我们,算法需要多少额外的空间来存储数据或执行计算。
- O(1) 常数空间复杂度:算法所占用的空间是固定的,不随输入数据规模的增加而增加。
例子:int sum = 0; for (int i = 0; i < n; i++) { sum += array[i]; } // 使用了一个固定大小的变量sum,空间复杂度为O(1)。
- O(n) 线性空间复杂度:算法所占用的空间与输入数据的规模成正比。
例子:int[] newArray = new int[n]; // 创建一个与输入数据规模相同的新数组,空间复杂度为O(n)。
- O(n^2) 平方空间复杂度:算法所占用的空间与输入数据规模的平方成正比。
例子:创建一个二维数组int[][] matrix = new int[n][n]; // 空间复杂度为O(n^2)。
4.3 将“昂贵”的时间复杂度转换成“廉价”的空间复杂度
例如,对于固定数据量的输入,这段代码需要消耗 1 年的时间去完成计算。如果在跑程序的 1 年时间内,出现了断电、断网或者程序抛出异常等预期范围之外的问题,那很可能造成 1 年时间浪费的惨重后果。很显然,用 1 年的时间去跑一段代码,对开发者和运维者而言都是极不友好的。
所以,空间是廉价的,最不济也是可以通过购买更高性能的计算机进行解决的。然而时间是昂贵的,如果无法降低时间复杂度,那系统的效率就永远无法得到提高。
五、我们如何知道什么时候应该使用数据结构与算法?应该是什么数据结构吗?
我们先来看看有哪些数据结构和算法。
5.1 数据结构分类
数据结构就像是存放数据的“容器”,每种“容器”都有自己的特点和用途。
一、数组(Array):就像是一个有多个抽屉的柜子,每个抽屉里可以放一个东西(数据)。你知道每个抽屉的位置,所以可以很快地找到你想找的东西。比如,你想找抽屉里的第三本书,你不需要打开前两个抽屉,直接打开第三个就可以了。数组适合存储需要快速访问的数据。
例子:你想存储10个学生的分数,可以用一个数组,scores[0]存储第一个学生的分数,scores[1]存储第二个学生的分数,以此类推。
二、 链表(Linked List):就像是串在一条线上的珠子,每个珠子代表一个数据,而且每个珠子还知道下一个珠子的位置。这种结构适合需要频繁插入或删除数据的情况,因为你不需要移动其他数据来腾出空间或填补空缺。
例子:你想记录一个学生的选课记录,每当学生选一门课,你就把一个课程节点“串”到链表的末尾。
三、栈(Stack):想象你有一个只能放一个物品的盒子,你每次只能把新物品放在最上面,取物品时也只能取最上面的那个。这就是栈的特点:后进先出(LIFO)。
例子:你正在用计算器做计算,你输入了一个数字,然后又输入了一个操作符(比如+),这时你想改变主意,用乘法代替加法,你可以“撤销”加法操作符,就像从栈中弹出一个元素一样。
四、队列(Queue):想象你在排队买电影票,买完,你就可以去看电影了,而后面的必须等待前面的买完走了,他才能买,然后才能看电影。这就是队列的特点:先进先出(FIFO)。
例子:你在一个网站上下单购物,你的订单会被加入到处理队列中,网站会按照下单的先后顺序来处理订单。
五、树(Tree):在计算机的文件系统中,文件目录通常被组织成一棵树。根目录是树的根节点,每个文件夹是一个节点,文件夹内的文件和子文件夹是该节点的子节点。
六、图(Graph):在社交网络中,用户是节点,用户之间的好友关系是边。这是一个典型的无向图,因为好友关系是双向的。
5.2 算法分类
算法就是解决问题的方法或步骤。
一、排序算法:比如你有一堆书,你想按照书名的字母顺序来排列它们。你可以一本一本地比较书名,然后交换它们的位置,直到所有书都按顺序排列好。这就是排序算法的一种。
例子:快速排序算法。它首先找一个“基准”元素,然后把所有比基准小的元素放在基准的左边,所有比基准大的元素放在基准的右边。然后,它对左边和右边的子数组重复这个过程,直到每个子数组都只有一个元素或为空。
二、 搜索算法:比如你想在一堆书里找一本特定的书,你可能会一本一本地看,直到找到那本书。这就是搜索算法的一种。
例子:二分搜索算法。它要求书必须是按字母顺序排列的。你先找到中间的书,如果书名正是你要找的,搜索结束;如果书名比你要找的大,你在左边的子数组中继续搜索;如果书名比你要找的小,你在右边的子数组中继续搜索。这个过程会一直重复,直到找到那本书或确定那本书不在集合中。
5.3 在什么时候我们应该使用什么样的数据结构
- 当你需要快速访问数据时,也就是查询时,使用数组。
- 当你需要频繁插入或删除数据,而且不关心数据的顺序时,也就是增删改时,使用链表。
- 当你需要“撤销”最近的操作时,使用栈。
- 当你需要按照处理的先后顺序来处理数据时,使用队列。
好了,内容就介绍到这里,我是前期后期,如果大家有内容想要讨论,可以在评论区分享,我们下一篇文章见。