(给ImportNew加星标,提高Java技能)
编译:ImportNew/覃佑桦
www.baeldung.com/java-greedy-algorithms
1.引言
本文将介绍如何用 Java 实现贪心算法。
2.贪心问题
通常一个数学问题会有几种解法。可以迭代解决,也可以用分治法(快速排序算法)或者动态规划(背包问题)这样的高级方法。
大多数情况我们都在寻找最优解,但可悲的是并非每次都能得到期望的结果。某些情况下,即使是次优解也很有用。借助一些策略或者启发式方法,我们能得到这样的宝贵结果。
假定问题可以分解,并且在过程的每一步都选择局部最优即“贪心选择”,那么可以称为贪心算法。
这里的重点是问题“可拆分”:即问题可以被描述为一组相似的子问题。大多数情况下,贪心算法都采用递归实现。
即使有诸多限制,像计算资源限制、限制执行时间、API 或其它限制,贪心算法仍然有办法找到合理的解决方案。
2.1.问题
作为示例,接下来会实现一个贪心策略通过 API 从社交网络提取数据。
假设我们希望在社交网络“小蓝鸟”上吸引更多的用户。要达成目标最好的方法是发布原创内容或者转发,引起大家的兴趣。
如何找到感兴趣的观众呢?好吧,我们必须找到一个流量大 V,为他们发布一些内容。
2.2.经典算法 vs 贪心算法
考虑下面的情况:如下图所示,大 V 帐户有4个粉丝,每个粉丝分别又有2个、2个、1个和3个关注者,以此类推:

带着之前的目标,我们会挑选粉丝较多的关注者。接下来把这个过程重复两次以上,直到抵达第3级(共4级)。
通过这种方式我们确定了一组粉丝最多的用户。如果可以向这组用户推荐,那么他们中一定会有人打开页面。
先从“传统方式”开始。每一次都查询关注此帐户的粉丝。随着选择地进行,查询的帐户数量会逐渐增加。
令人吃惊的是,总共要执行25次查询:

这里有一个问题:Twitter API 的查询限制是每15分钟最多查询15次。如果超过调用限制,则会报告 “Rate limit exceeded code – 88” 或者“API v1.1 由于应用程序 rate limit 达到限值无法处理请求”。如何突破限制?
远在天边,近在眼前:贪心算法。在贪心算法中,每一步只有粉丝最多的用户才是我们考虑的对象:结果只需要4次查询,效果显著!

这两种方法的结果将有所不同。使用传统方法,最好的情况下需要16次查询,而贪心算法最多需要12次。
这种差异是否真的有价值?这个稍后再讨论。
3.实现
为了实现上述逻辑,我们做了一个 Java 程序模拟 Twitter API。此外,还用到了Lombok 开发库。
定义SocialConnector 实现代码逻辑。注意:包含的 counter 模拟调用限制,为方便测试把限制减少到4次:
public class SocialConnector {
private boolean isCounterEnabled = true;
private int counter = 4;
@Getter @Setter
List users;
public SocialConnector() {
users = new ArrayList<>();
}
public boolean switchCounter() {
this.isCounterEnabled = !this.isCounterEnabled;
return this.isCounterEnabled;
}
}
接着添加 getFollowers 方法获取粉丝列表:
public List getFollowers(String account) {
if (counter < 0) {
throw new IllegalStateException ("API limit reached");
} else {
if (this.isCounterEnabled) {
counter--;
}
for (SocialUser user : users) {
if (user.getUsername().equals(account)) {
return user.getFollowers();
}
}
}
return new ArrayList<>();
}
为了流程需要,还需要添加 User 实体类:
public class SocialUser {
@Getter
private String username;
@Getter
private List followers;
@Overridepublic boolean equals(Object obj) {return ((SocialUser) obj).getUsername().equals(username);
}public SocialUser(String username) {this.username = username;this.followers = new ArrayList<>();
}public SocialUser(String username, List followers) {this.username = username;this.followers = followers;
}public void addFollowers(List followers) {this.followers.addAll(followers);
}
}
3.1.贪心算法
最后实现贪心算法,添加 GreedyAlgorithm 开始实现递归:
public class GreedyAlgorithm {
int currentLevel = 0;
final int maxLevel = 3;
SocialConnector sc;
public GreedyAlgorithm(SocialConnector sc) {
this.sc = sc;
}
}
需要添加一个 findMostFollowersPath 方法,找到粉丝最多的用户并对其进行计数,然后继续执行下一步:
public long findMostFollowersPath(String account) {
long max = 0;
SocialUser toFollow = null;
List followers = sc.getFollowers(account);
for (SocialUser el : followers) {
long followersCount = el.getFollowersCount();
if (followersCount > max) {
toFollow = el;
max = followersCount;
}
}
if (currentLevel < maxLevel - 1) {
currentLevel++;
max += findMostFollowersPath(toFollow.getUsername());
}
return max;
}
请记住:在这里进行贪心选择。每次调用 findMostFollowersPath 时,从列表中仅选择一个元素并继续:已选择的路径不再返回。
完美!准备就绪,可以开始测试我们的程序。在此之前需要初始化迷你关系网,然后启动单元测试:
public void greedyAlgorithmTest() {
GreedyAlgorithm ga = new GreedyAlgorithm(prepareNetwork());
assertEquals(ga.findMostFollowersPath("root"), 5);
}
3.2.非贪心算法
创建一个非贪心方法,看看执行效果。新建 NonGreedyAlgorithm 类:
public class NonGreedyAlgorithm {
int currentLevel = 0;
final int maxLevel = 3;
SocialConnector tc;
public NonGreedyAlgorithm(SocialConnector tc, int level) {
this.tc = tc;
this.currentLevel = level;
}
}
实现一个功能等价的 findMostFollowersPath 方法:
public long findMostFollowersPath(String account) {
List followers = tc.getFollowers(account);long total = currentLevel > 0? followers.size() : 0;if (currentLevel < maxLevel ) {
currentLevel++;long[] count = new long[followers.size()];int i = 0;for (SocialUser el : followers) {
NonGreedyAlgorithm sub = new NonGreedyAlgorithm(tc, currentLevel);
count[i] = sub.findMostFollowersPath(el.getUsername());
i++;
}long max = 0;for (; i > 0; i--) {if (count[i-1] > max) {
max = count[i-1];
}
}return total + max;
}return total;
}
准备相应的单元测试:一个用来验证是否超出调用限制,另一个用来检查使用非贪心算法的返回值:
public void nongreedyAlgorithmTest() {
NonGreedyAlgorithm nga = new NonGreedyAlgorithm(prepareNetwork(), 0);
Assertions.assertThrows(IllegalStateException.class, () -> {
nga.findMostFollowersPath("root");
});
}
public void nongreedyAlgorithmUnboundedTest() {
SocialConnector sc = prepareNetwork();
sc.switchCounter();
NonGreedyAlgorithm nga = new NonGreedyAlgorithm(sc, 0);
assertEquals(nga.findMostFollowersPath("root"), 6);
}
4.结果
回顾一下前面的工作。
首先,我们尝试了贪心算法并验证了其有效性。然后,在启用和取消 API 限制的情况下验证了传统方法的结果(穷尽式搜索)。
贪心算法能快速返回结果,每次都选择局部最优解。相反,由于 API 限制,非贪心算法可能无法返回结果(抛异常)。
比较两种方法及结果可以看到,即使得到的不是最优解,贪心算法也能为我们解忧。可以把它称为局部最优解。
5.总结
在社交媒体瞬息万变今天,找到最优解也许已经成为一种不切实际的妄想:既难以达到,也不太现实。
如何克服限制与优化 API 调用是一个很大的课题,但正如本文讨论的那样,贪心算法是一种有效的办法。选择贪心算法能帮助我们减轻寻找最优解的痛苦,进而得到有价值的结果。
请记住,贪心算法也非银弹,不是每种情况都适用:需要具体问题具体分析。
本文中的示例代码可在 GitHub 上找到。
github.com/eugenp/tutorials/tree/master/algorithms-miscellaneous-5
推荐阅读 点击标题可跳转Java 异步编程:Loom 项目介绍
Java8 的 Stream 对集合操作飞起来
2020 年最棒的 9 个 Java 框架,哪个最香?
看完本文有收获?请转发分享给更多人
关注「ImportNew」,提升Java技能

好文章,我在看❤️
本文介绍如何使用Java实现贪心算法解决社交网络数据提取问题,通过对比贪心算法与传统穷举方法,展示了贪心算法在面对API调用限制时的优势。
7670

被折叠的 条评论
为什么被折叠?



