【数据结构与算法基础】代码仓库:https://github.com/jinrunheng/datastructure-and-algorithm

一:集合基础

什么是集合?

集合(Set)是基本的数学概念,指的是具有某种特定性质的事物的总体。

集合有以下几种特性:

  • 无序性

一个集合中,每个元素的地位都是相同的,元素之间是无序的

  • 互异性

一个集合中,任何两个元素都认为是不相同的,即每个元素只能出现一次

  • 确定性

给定一个集合,任意一个元素。该元素只存在属于或不属于该集合两种情况,不允许有模棱两可的情况出现

集合接口定义的方法如下:
image.png

二:基于二分搜索树的集合实现

代码链接🔗

我们可以使用我们自己的二分搜索树来实现集合,因为我们的二分搜索树不支持插入重复的元素,所以使用它作为底层也符合集合的互异性。

三:基于链表的集合实现

代码链接🔗

同样地,我们也可以使用我们自己的链表来实现集合,不过在添加元素时,我们需要判断添加的元素是否在当前链表中已经存在,如果没有存在则可以添加,如果已存在则不能添加。

四:集合的复杂度分析

测试

对于我们实现的 BSTSet(基于二分搜索树的集合实现)与 LinkedListSet(基于链表的集合实现)二者性能有多大的差异?我们来具体测试一下:

  1. package com.github.datastructureandalgorithm.datastructure.Set;
  2. import org.junit.jupiter.api.Test;
  3. import java.util.ArrayList;
  4. import java.util.List;
  5. import java.util.UUID;
  6. public class SetPerformanceTest {
  7. private static List<String> generateTestList() {
  8. List<String> list = new ArrayList<>();
  9. for (int i = 0; i < 100000; i++) {
  10. String s = UUID.randomUUID().toString();
  11. list.add(s);
  12. if (i % 3 == 0) {
  13. list.add(list.get(list.size() - 1));
  14. }
  15. }
  16. return list;
  17. }
  18. private static double testSet(Set<String> set, List<String> strings) {
  19. long startTime = System.nanoTime();
  20. for (String s : strings) {
  21. set.add(s);
  22. }
  23. long endTime = System.nanoTime();
  24. return (endTime - startTime) / 1000000000.0;
  25. }
  26. @Test
  27. void performanceTest() {
  28. List<String> strings = generateTestList();
  29. BSTSet<String> bstSet = new BSTSet<>();
  30. LinkedListSet<String> linkedListSet = new LinkedListSet<>();
  31. double time1 = testSet(bstSet, strings);
  32. System.out.println("BST Set : " + time1 + " s");
  33. double time2 = testSet(linkedListSet, strings);
  34. System.out.println("LinkedList Set : " + time2 + " s");
  35. }
  36. }

代码很简单,在这里就不过多说明了;对于十万这个量级的数据,我的电脑运行测试结果如下:

  1. BST Set : 0.129656602 s
  2. LinkedList Set : 28.116667089 s

可以看到,二者的差异是巨大的。

复杂度分析

对于底层为链表的集合来说,添加,查找,删除元素的时间复杂度均为 ,而底层为二分搜索树的集合则不同,让我们回想一下向二分搜索树中添加元素的逻辑:

image.png
与链表不同,向二分搜索树中添加一个元素,最多只需要走这棵树的“高度”或“深度”。在这里我们使用“高度”这个词来描述。我们将二叉树的高度记作 h,很显然,对于底层为二分搜索树的集合来说,添加,查找,删除元素的时间复杂度均为 。

h 与 N 的关系是怎样的呢?

当一棵二叉树为满二叉树时,h 与 N 满足关系式: 即:,所以,此时对于底层为二分搜索树的集合来说,它的增,删,查找的时间复杂度为:

不过, 这个时间复杂度是我们考虑的最优的情况,如果我们的二分搜索树的节点分布“均衡”,时间复杂度自然是 ;可是如果出现了最差的情况,二分搜索树就会退化为一个链表:

image.png
而此时,这棵二分搜索树与链表无异,增,删,查找的时间复杂度将退化为 。

复杂度:


LinkedListSet BSTSet(平均) BSTSet(最差)
add O(N) O(logN) O(N)
contains O(N) O(logN) O(N)
remove O(N) O(logN) O(N)

五:映射基础

映射(Map)指的是两个元素的集合之间元素相互“对应”的关系。在数学及相关的领域等同于函数。

image.png
映射的基本概念:

  • 映射是一种以键值对形式存储的数据结构(key,value)
  • 映射可以根据键(key),寻找值(value)
  • 映射中,键是唯一的,一个值可能对应多个键,但是一个键只能对应一个值。

映射的接口:
image.png

六:基于链表的映射实现

代码链接🔗

以链表作为底层数据结构实现映射。

七:基于二分搜索树的映射实现

代码链接🔗

以二分搜索树作为底层数据结构实现映射。

八:映射的复杂度分析

测试

为了观察我们实现的 BSTMap(基于二分搜索树的映射实现)与 LinkedListMap(基于链表的映射实现)二者性能的差异,我们使用代码进行测试:

  1. package com.github.datastructureandalgorithm.datastructure.Map;
  2. import org.junit.jupiter.api.Test;
  3. import java.util.ArrayList;
  4. import java.util.List;
  5. import java.util.Random;
  6. import java.util.UUID;
  7. public class MapPerformanceTest {
  8. private static List<String> generateTestList() {
  9. List<String> list = new ArrayList<>();
  10. for (int i = 0; i < 100000; i++) {
  11. String s = UUID.randomUUID().toString();
  12. list.add(s);
  13. if (i % 3 == 0) {
  14. int random = new Random().nextInt(list.size());
  15. list.add(list.get(random));
  16. }
  17. }
  18. return list;
  19. }
  20. private static double testMap(Map<String, Integer> map, List<String> list) {
  21. long startTime = System.nanoTime();
  22. for (String word : list) {
  23. if (map.contains(word)) {
  24. map.set(word, map.get(word) + 1);
  25. } else {
  26. map.add(word, 1);
  27. }
  28. }
  29. long endTime = System.nanoTime();
  30. return (endTime - startTime) / 1000000000.0;
  31. }
  32. @Test
  33. void performanceTest() {
  34. LinkedListMap<String, Integer> linkedListMap = new LinkedListMap<>();
  35. BSTMap<String, Integer> bstMap = new BSTMap<>();
  36. List<String> words = generateTestList();
  37. double time1 = testMap(linkedListMap, words);
  38. System.out.println("LinkedListMap : " + time1 + " s");
  39. double time2 = testMap(bstMap, words);
  40. System.out.println("BSTMap : " + time2 + " s");
  41. }
  42. }

对于十万这个量级的数据,我的电脑运行测试结果如下:

  1. LinkedListMap : 84.925896898 s
  2. BSTMap : 0.163672168 s

复杂度分析

上面的测试结果差异之大,究其原因,和我们上面用链表与二分搜索树实现集合分析的结果是一样的。BSTMap 平均下来看,整体的时间复杂度为 ,而 LinkedListMap 所有操作的时间复杂度均为 。


LinkedListMap BSTMap(平均) BSTMap(最差)
add O(N) O(logN) O(N)
remove O(N) O(logN) O(N)
set O(N) O(logN) O(N)
get O(N) O(logN) O(N)
contains O(N) O(logN) O(N)