在两份数据里面找相同ID的数据,很多人会写两个for循环嵌套。这个写法效率比较低,今天来看一个提高速度的优化案例。
场景示例:比如现在有两个List数据,一个是 User List 集合,另一个是 UserMemo List 集合。我们需要先遍历User List,然后根据userId从UserMemo List里面取出这个userId对应的content值,做数据处理。
User实体类:
import lombok.Data;
@Data
public class User {
private Long userId;
private String name;
}
UserDemo实体类:
import lombok.Data;
@Data
public class UserDemo {
private Long userId;
private String content;
}
模拟数据集合5W条user数据,3W条userMemo数据:
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class DataSet {
public static List getUserTestList() {
List users = new ArrayList<>();
for (int i = 1; i <= 50000; i++) {
User user = new User();
user.setName(UUID.randomUUID().toString());
user.setUserId((long) i);
users.add(user);
}
return users;
}
public static List getUserDemoTestList() {
List userDemos = new ArrayList<>();
for (int i = 1; i <= 30000; i++) {
UserDemo userDemo = new UserDemo();
userDemo.setContent(UUID.randomUUID().toString());
userDemo.setUserId((long) i);
userDemos.add(userDemo);
}
return userDemos;
}
}
平时不注意的时候可能会这样去写代码处理 :
import org.springframework.util.StopWatch;
import java.util.List;
public class Demo {
public static void main(String[] args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start("业务处理");
List userList = DataSet.getUserTestList();
List userDemoList = DataSet.getUserDemoTestList();
for (User user : userList) {
Long userId = user.getUserId();
for (UserDemo userDemo : userDemoList) {
if (userId.equals(userDemo.getUserId())) {
String content = userDemo.getContent();
System.out.println("模拟数据content业务处理......" + content);
}
}
}
stopWatch.stop();
System.out.println("TaskName: " + stopWatch.getLastTaskName() + " --》 耗时(单位:秒):" + stopWatch.getTotalTimeSeconds() + ",耗时(单位:毫秒):" + stopWatch.getTotalTimeMillis());
}
}
其实数据量小的话,其实没多大性能差别,不过我们还是需要知道一些技巧点。我们来看看这时候的一个耗时情况,相当于迭代了 5W * 3W 次 ,可以看到用时是13721毫秒 :
模拟数据content业务处理......0fdc5f52-27ed-46ba-917f-6afbcb82625b
模拟数据content业务处理......d0f7fdd0-a6b9-46fa-8723-d6225fdbae7f
模拟数据content业务处理......d739eba1-7ada-4872-89cf-0157caf03a57
TaskName: 业务处理 --》 耗时(单位:秒):13.7213107,耗时(单位:毫秒):13721
其实到这,插入个题外点,如果说每个userId在UserMemo List里面都是只有一条数据的场景。单从上面这段代码来看,还是有问题的。因为当我们从内循环UserMemo List里面找到匹配数据的时候, 就没有做其它操作了。
这样内for循环会继续下,直到跑完再进行下一轮整体循环。所以,仅针对这种情形,1对1的或者说我们只需要找到一个匹配项,处理完后我们应该使用break。
我们来看看加上break的一个耗时情况:
for (User user : userList) {
Long userId = user.getUserId();
for (UserDemo userDemo : userDemoList) {
if (userId.equals(userDemo.getUserId())) {
String content = userDemo.getContent();
System.out.println("模拟数据content业务处理......" + content);
break;
}
}
}
耗时情况:可以看到 从13秒多变成了10秒多, 这个break 加的很OK。
模拟数据content业务处理......f25e98f2-cbd6-483f-ad7a-600e03c489dd
模拟数据content业务处理......76db1b83-47cf-40bd-a574-933b15c60744
模拟数据content业务处理......00fcf7e5-d57b-47bb-baa6-8e148d43be31
TaskName: 业务处理 --》 耗时(单位:秒):10.6829932,耗时(单位:毫秒):10682
回到我们刚才, 平时需要for循环里面再for循环这种方式,可以看到耗时是13秒多。那如果场景更复杂一点, 是for循环里面含有多个for循环,那这样代码耗时真的非常恐怖。
那么接下来这个技巧点是使用map 去优化 (提前把list转成我们要用的map结构数据,根据userId直接从map里面找到content的value。)
import cn.hutool.core.util.StrUtil;
import org.springframework.util.StopWatch;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Demo {
public static void main(String[] args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start("业务处理");
List userList = DataSet.getUserTestList();
Map contentMap = DataSet.getUserDemoTestList().stream().collect(Collectors.toMap(UserDemo::getUserId, UserDemo::getContent));
for (User user : userList) {
Long userId = user.getUserId();
String content = contentMap.get(userId);
if (StrUtil.isNotBlank(content)) {
System.out.println("模拟数据content业务处理......" + content);
}
}
stopWatch.stop();
System.out.println("TaskName: " + stopWatch.getLastTaskName() + " --》 耗时(单位:秒):" + stopWatch.getTotalTimeSeconds() + ",耗时(单位:毫秒):" + stopWatch.getTotalTimeMillis());
}
}
耗时情况:可以看到 从10秒多变成了0.86秒多, 这个map改造是相当OK。
模拟数据content业务处理......1b780dc0-98c1-4284-b49c-002b9e4367e9
模拟数据content业务处理......b02a28ec-f1bf-474d-a5dd-822d70a5cc4b
模拟数据content业务处理......c1d57314-b2b7-4b83-950a-b87cea361840
TaskName: 业务处理 --》 耗时(单位:秒):0.8624439,耗时(单位:毫秒):862
为什么效果这么显著?
答:这其实就是时间复杂度,for循环嵌套for循环,就好比循环每一个user,拿出userId在里面的循环userDemo list集合中按顺序去开盲盒匹配,拿出第一个,看看userId ,拿出第二个,再看看userId ,一直找匹配的。而我们提前对userDemo list集合做一次遍历,转存储在map里面 。map的取值效率在多数的情况下是能维持接近O(1) 的,毕竟数据结构摆着,数组加链表。相当于拿到userId在去开盲盒的时候, 根据userId这个key hash中能直接找到数组里面的索引标记位, 如果底下没链表(有的话时间复杂度为O(logn)),直接取出来就完事了。
按照目前以JDK8 的hash算法,起hash冲突的情况是非常非常少见了。最恶劣的情况,只有当全部key都冲突, 全都分配到一个桶里面去都占用一个位置 ,这时候就是O(n),这种情景不需要去考虑。