JavaRush /Java 博客 /Random-ZH /摸索算法或轻松介绍算法
Viacheslav
第 3 级

摸索算法或轻松介绍算法

已在 Random-ZH 群组中发布
《Grocking Algorithms》一书的评论。一点个人看法,举几个例子。我希望这篇评论能帮助您了解您是否想读这本书,或者它是否不会在您的书架上占据一席之地。警告:大量文字)

“增长算法”或轻松介绍算法

介绍

几乎所有初级职位空缺都有“数据结构和算法知识”等要求。对于受过专业教育的人来说,算法属于普通课程,应该没有问题。但如果这些发展是从其他草原引入的呢?剩下的就是自己学习了。“谁该责备”这个问题是有答案的,但“该做什么”这个问题必须寻求答案。我们去书本上看看吧。我想告诉你一个。
“摸索算法”或简单的算法介绍 - 1

Grok 算法

在所有的著作中,我看到了《Grocking Algorithms》这样一本书。您可以在这里找到更多信息:“ 《增长算法。为程序员和好奇者提供的图解指南》一书。” 我很久以前就注意到这本书了,但是臭氧价格是680卢布。贵还是便宜——每个人自己决定。我已经以一半的价格购买了关于 Avito 的第二本书。所以我在圣彼得堡找到了它,买了它,然后去探索。这就是我决定与大家分享的内容。是的,书中没有 Java 代码,但有……其他代码,稍后会详细介绍。

算法简介(选择排序)

因此,以一种简单的叙述形式,我们达到了表演的第一个排序。这就是选择排序。它的本质是我们从左到右(从0个元素到最后一个)遍历元素,并在剩余元素中寻找最大的。如果找到它,那么我们就交换现在所在的元素和最大的元素。首先想到数组的最简单的方式是:[5, 3, 6, 2, 10]。拿一张纸,一支笔(最简单、最便宜的方式),想象一下我们如何有一个左边框(左),一个当前索引(或右边框),有一个最小元素的索引。以及我们如何使用它。例如:
“摸索算法”或简单的算法介绍 - 2
算法通常以伪代码描述,例如在维基百科上。我们的代码并不完全是伪代码,稍后会详细介绍。现在让我们看看:

def selectionSort(array):
    for left in range(0, len(array)):
        minIndex = left
        for right in range (left+1, len(array)):
            if array[right] < array[minIndex]:
                minIndex = right
        if minIndex != left:
            temp = array[left]
            array[left] = array[minIndex]
            array[minIndex] = temp
    return array

print(selectionSort([5, 3, 6, 2, 10]))
现在我们以Java代码的形式来呈现:
public static void selectionSort(int[] array) {
        for (int left = 0; left < array.length; left++) {
            int minIndex = left;
            for (int right = left+1; right < array.length; right++) {
                if (array[right] < array[minIndex]) {
                    minIndex = right;
                }
            }
            if (minIndex != left) {
                int temp = array[left];
                array[left] = array[minIndex];
                array[minIndex] = temp;
            }
        }
}
正如您所看到的,代码几乎是相同的。第一个代码是书中的示例。第二个是我在Java代码中的自由执行。

递归

接下来我们被告知有递归这样的东西。首先,存在一个关于拥有 AxB 大小田地的农民的问题。有必要将这个领域分成相等的“正方形”。然后提到欧几里得算法。我不喜欢的是他们没有尝试编写其代码。但欧几里得算法简单有效:
“摸索算法”或轻松介绍算法 - 3
老实说,我错过了书中的一些细节,比如这个视频:“信息学。算法理论。欧几里得算法。” 例如,如果 a 小于 b,则在第一次运行期间 b 和 a 将交换位置,第二次时较大的值将除以较小的值。因此,参数的顺序并不重要。像往常一样,首先我们可以在一张纸上“感受”算法:
“摸索算法”或轻松介绍算法 - 4
现在我们看一下代码:

def euclidean(a, b):
    if a == 0 : return b
    if b == 0 : return a
    return euclidean (b, a % b)
让我们用 Java 编写相同的代码。如果需要,我们可以使用在线编译器
public static int euclid(int a, int b) {
        if (a == 0) return b;
        if (b == 0) return a;
        return euclid(b, a%b);
}
本书开头也提到了阶乘。数字 n (n!) 的阶乘是从 1 到 n 的数字的乘积。为什么要这样做?这里有一个实际应用。如果我们有 n 个对象(例如,n 个城市),那么我们可以制作 n 个!组合。您可以在此处阅读有关递归的更多信息:“递归。训练任务。” 迭代和递归方法的比较:“递归”。

快速排序

快速排序是一个相当有趣的算法。书中并没有过多关注他。此外,仅给出最坏情况(当选择第一个元素时)的代码。然而,也许对于第一次认识的人来说这个例子会更容易记住。写一个糟糕的快速排序比根本不写要好。这是书中的一个例子:

def quicksort(array):
    if len(array) < 2:
        return array
    else:
        pivot = array[0]
        less = [i for i in array[1:] if i <= pivot]
        greater = [i for i in array[1:] if i > pivot]
    return quicksort(less) + [pivot] + quicksort(greater)
这里的一切都超级简单。如果我们有一个包含 0 或 1 个元素的数组,则无需对其进行排序。如果它更大,我们就取出数组的第一个元素并将其视为“枢轴元素”。我们创建 2 个新数组 - 第一个包含比主元大的元素,第二个包含比主元小的元素。我们递归地重复。这不是最好的选择,但还是最好记住。让我们用 Java 实现这个算法,但更正确。评论“ JavaScript 中的计算机科学:快速排序”中的材料将帮助我们解决这个问题。在写代码之前,我们再画一下来“感受一下”算法:首先,我们在一张纸上再画一下,来理解一下算法:
“摸索算法”或轻松介绍算法 - 5
在我看来,最危险的时刻之一就是彻底解决问题。因此,我们将分几个小步骤来进行实施:
  • 我们需要能够交换数组中的元素:

    private static void swap(int[] array, int firstIndex, int secondIndex) {
            int temp = array[firstIndex];
            array[firstIndex] = array[secondIndex];
            array[secondIndex] = temp;
    }

  • 我们需要一个方法,将指定区间内的数组分为 3 部分


    private static int partition(int[] array, int left, int right) {
            int pivot = array[(right + left) / 2];
            while (left <= right) {
                while (array[left] < pivot) {
                    left++;
                }
                while (array[right] > pivot) {
                    right--;
                }
                if (left <= right) {
                    swap(array, left, right);
                    left++;
                    right--;
                }
            }
            return left;
    }

    详情见上面的链接。简而言之,我们移动左光标,直到该元素小于枢轴。同样,从另一端移动右光标。如果光标不匹配,我们会进行交换。我们继续直到光标收敛。我们返回一个索引,将进一步处理分为两部分。

  • 有一个分离,我们需要排序本身:

    public static void quickSort(int[] array, int left, int right) {
            int index = 0;
            if (array.length > 1) {
                index = partition(array, left, right);
                if (left < index - 1) {
                    quickSort(array, left, index - 1);
                }
                if (index < right) {
                    quickSort(array, index, right);
                }
            }
    }

    也就是说,如果数组至少包含两个元素,那么它们就已经可以排序了。首先,我们将整个数组分为两部分,小于主元的元素和大于主元的元素。然后我们对每个结果部分执行类似的操作。

    对于测试:

    public static void main(String []args){
            int[] array = {8,9,3,7,6,7,1};
            quickSort(array, 0, array.length-1);
            System.out.println(Arrays.toString(array));
    }
书中指出,该算法属于所谓的“分而治之”算法,即每次将处理的数据集一分为二。算法复杂度:O(nLogn)
“摸索算法”或轻松介绍算法 - 6
不好的(也就是我不喜欢的)是这本书顺便提到了合并排序,但没有提供任何示例或代码。更多详细信息可以在这里找到:“信息学。搜索和排序算法:归并排序”。因此,为了保持一致性,我们还是自己来做吧。当然,算法本身本质上是简单明了的:
public static void mergeSort(int[] source, int left, int right) {
    if ((right - left) > 1) {
        int middle = (right + left) / 2;
        mergeSort(source, left, middle);
        mergeSort(source, middle + 1, right);
    }
    merge(source, left, right);
}
我们确定中间位置并将数组分成两半。对于每一半,我们都做同样的事情,依此类推。停止条件或基本情况 - 我们必须有多个元素,因为我们不能将一个元素一分为二。现在我们需要实现合并,即merge:
public static void merge(int[] array, int from, int to) {
    int middle = ((from + to) / 2) + 1;
    int left = from;
    int right = middle;
    int cursor = 0;

    int[] tmp = new int[to - from + 1];
    while (left < middle || right <= to) {
        if (left >= middle) {
            tmp[cursor] = array[right];
            System.out.println("Остаток справа: " + array[right]);
            right++;
        } else if (right > to) {
            tmp[cursor] = array[left];
            System.out.println("Остаток слева: " + array[left]);
            left++;
        } else if (array[left] <= array[right]) {
            tmp[cursor] = array[left];
            System.out.println("Слева меньше: " + array[left]);
            left++;
        } else if (array[right] < array[left]) {
            tmp[cursor] = array[right];
            System.out.println("Справа меньше: " + array[right]);
            right++;
        }
        cursor++;
    }
    System.arraycopy(tmp, 0, array, from, tmp.length);
}
这里没有太多可评论的。从变量的名称来看,println一切都清楚了。好吧,检查一下:
int array[] = {1, 7, 3, 6, 7, 9, 8, 4};
mergeSort(array, 0, array.length - 1);
System.out.println(Arrays.toString(array));

哈希表

本书还涉及哈希表。您不必自己实现它,哈希表的本质非常简单。毕竟Java也有哈希表的实现,java.util.HashTable。如果我们查看 HashTable 设备,我们会看到 Entry 数组位于其中。Entry是一个由Key-Value组合而成的记录。HashTable有initialCapacity——即初始大小。loadFactor——负载因子。默认值为 0.75。这个数字告诉您在数组负载达到什么程度(元素数/总量)时需要增加大小。在Java中它增加了2倍。书中解释说,哈希表之所以称为哈希表,是因为基于哈希函数,其中的数组单元(篮子)Entry。您还可以在这里阅读更多内容:图片中的数据结构。HashMapLinkedHashMap。你也可以在书上读到。例如这里:“哈希表基础知识

图,广度优先搜索(最短路径搜索)

图表可能是最有趣的主题之一。公平地说,本书对它们给予了很多关注。也许这就是这本书值得一读的原因。虽然也许可以说得更清楚一些))但是,我们有互联网,除了这本书之外,您还可以查看这个关于“那些第一次听说图表的人”的理论的播放列表。 ” 嗯,自然地,在本书的一开始,breadth-first-search就给出了广度优先搜索算法,也称为 BFS。书中包含以下图表:
“摸索算法”或轻松介绍算法 - 7
书中指出队列会对我们有所帮助。此外,我们可以将元素添加到末尾并从头开始处理队列。这样的队列称为双向队列,英文称为Deque。书中建议使用一种数据结构——哈希表。将名称和邻居关联起来。对于编号的顶点,您可以简单地使用数组。这种顶点存储称为“相邻顶点列表”,书中没有提及。这对他们来说是一个缺点。让我们用 Java 来实现这个:
private Map<String, String[]> getGraph() {
    Map<String, String[]> map = new HashMap<>();
    map.put("you", new String[]{"alice", "bob", "claire"});
    map.put("bob", new String[]{"anuj", "peggy"});
    map.put("alice", new String[]{"peggy"});
    map.put("claire", new String[]{"thom", "jonny"});
    map.put("annuj", null);
    map.put("peggy", null);
    map.put("thom", null);
    map.put("johny", null);
    return map;
}
现在搜索本身是建立在这些数据的基础上的:
private String search() {
    Map<String, String[]> graph = getGraph();
    Set<String> searched = new HashSet<>();
    Deque<String> searchQue = new ArrayDeque<>();
    searchQue.add("you");
    while (!searchQue.isEmpty()) {
        String person = searchQue.pollFirst();
        System.out.println(person);
        if (personIsSeller(person)) {
            return person;
        } else {
            String[] friends = graph.get(person);
            if (friends == null) continue;
            for (String friend : friends) {
                if (friend != null && !searched.contains(friend)) {
                    searchQue.addLast(friend);
                }
            }
        }
    }
    return null;
}
如您所见,没什么复杂的。和书上的代码对比一下,几乎是一样的。

图,Dijkstra 算法

或多或少了解了 BFS,本书作者邀请我们了解 Daysktra 算法和加权图。建议下图作为解决方案:
“摸索算法”或轻松介绍算法 - 8
首先,我们需要了解如何表示我们的图表。我们可以将其表示为矩阵。关于 Habré 的文章将对我们有所帮助:Dijkstra 算法。在图表上寻找最佳路线。让我们使用邻接矩阵:
public Integer[][] getGraphMatrix(int size) {
    Integer[][] matrix = new Integer[size][size];
    matrix[0][1] = 6;
    matrix[0][2] = 2;
    matrix[2][1] = 3;
    matrix[1][3] = 1;
    matrix[2][3] = 5;
    return matrix;
}
现在逻辑本身:
@Test
public void dijkstra() {
    Integer[][] graph = getGraphMatrix();           // Данные графа
    Integer[] costs = new Integer[graph.length];    // Стоимость перехода
    Integer[] parents = new Integer[graph.length];  // Родительский узел
    BitSet visited = new BitSet(graph.length);      // "Ферма" маркеров посещённости

    Integer w = 0;
    do {
        System.out.println("-> Рассматриваем вершину: " + w);
        Integer min = null;
        for (int i = 0; i < graph.length; i++) {    // Обрабатываем каждую дугу
            if (graph[w][i] == null) continue;      // Дуги нет - идём дальше
            if (min == null || (!visited.get(i) && graph[w][min] > graph[w][i])) {
                min = i;
            }
            if (costs[i] == null || costs[i] > costs[w] + graph[w][i]) {
                System.out.print("Меням вес с " + costs[i]);
                costs[i] = (costs[w] != null ? costs[w] : 0) + graph[w][i];
                System.out.println(" на " + costs[i] + " для вершины " + i);
                parents[i] = w;
            }
        }
        System.out.println("Вершина с минимальным весом: " + min);
        visited.set(w);
        w = min;
    } while (w != null);

    System.out.println(Arrays.toString(costs));
    printPath(parents, 3);
}

public void printPath(Integer[] parents, int target) {
    Integer parent = target;
    do {
        System.out.print(parent + " <- ");
        parent = parents[parent];
    } while (parent != null);
}
这本书一步一步地分解了它。如果你在互联网上添加一篇关于Habré的文章+看看代码,你就能记住它。我发现逐步分析有点混乱。但对于循序渐进的性质本身来说,这是一个优点。总体来说还可以,虽然还可以更好)

贪心算法

下一节专门讨论“贪婪算法”。本节很有趣,因为它使用了集合(java.util.Set)。最后我们可以明白为什么需要它。我们使用状态列表作为输入:
Set<String> statesNeeded = new HashSet();
statesNeeded.addAll(Arrays.asList("mt", "wa", "or", "id", "nv", "ut", "ca", "az" ));
还有涵盖其中一些州的广播电台列表:
Map<String, Set<String>> stations = new HashMap<>();
stations.put("kone", new HashSet(Arrays.asList("id", "nv", "ut")));
stations.put("ktwo", new HashSet(Arrays.asList("wa", "id", "mt")));
stations.put("kthree", new HashSet(Arrays.asList("or", "nv", "ca")));
stations.put("kfour", new HashSet(Arrays.asList("nv", "ut")));
stations.put("kfive", new HashSet(Arrays.asList("ca", "az")));
书中接着指出并解释了算法本身:
Set<String> finalStations = new HashSet();
while (!statesNeeded.isEmpty()) {
    String bestStation = null;
    Set<String> statesCovered = new HashSet();
    for (String station: stations.keySet()) {
        Set covered = new HashSet(statesNeeded);
        covered.retainAll(stations.get(station));
        if (covered.size() > statesCovered.size()) {
           bestStation = station;
           statesCovered = covered;
        }
    }
    statesNeeded.removeAll(statesCovered);
    finalStations.add(bestStation);
}
System.out.println(finalStations);

动态规划

本书还描述了应用“动态规划”方法的问题。任务给出:
“摸索算法”或简单的算法介绍 - 9
我们有一个 4 磅的袋子。您需要找到给定重量下最赚钱的商品。首先,让我们列出一个项目清单:
List<Thing> things = new ArrayList<>();
things.add(new Thing("guitar", 1, 1500));
things.add(new Thing("tape recorder", 4, 3000));
things.add(new Thing("notebook", 3, 2000));
现在算法本身:
int bagSize = 4;
int cell[][] = new int[things.size()][bagSize];
// Заполняем первую строку без условий
for (int i = 0; i < bagSize; i++) {
    cell[0][i] = things.get(0).cost;
}
// Заполняем оставшиеся
for (int i = 1; i < cell.length; i++) {
    for (int j = 0; j < cell[i].length; j++) {
        // Если вещь не влезает - берём прошлый максимум
        if (things.get(i).weight > j+1) {
            cell[i][j] = cell[i - 1][j];
        } else {
            // Иначе текущая стоимость + предыдущий максимум оставшегося размера
            cell[i][j] = things.get(i).cost;
            if (j + 1 - things.get(i).weight > 0) {
                cell[i][j] += cell[i-1][j + 1 - things.get(i).weight];
            }
        }
    }
}
System.out.println(Arrays.deepToString(cell));
还有一个有趣的任务是找到最相似的单词。很有趣,不是吗?更多详细信息请参见:LongestCommonSubsequence.java

搜索最近的邻居

书中还非常清楚地讲了k近邻算法:
“摸索算法”或轻松介绍算法 - 10
并给出计算公式:
“摸索算法”或简单的算法介绍 - 11

底线

本书最后有一个有趣的“下一步是什么?”部分,其中提供了有趣算法的快速概述。这里简单介绍一下树和其他算法的含义。总的来说,我喜欢这本书。不应将其视为某种综合信息。你必须自己去寻找和理解。但作为引起兴趣并给出初步想法的介绍性信息,这是相当不错的。是的,书中的代码是用Python编写的。所以上面所有的例子都是可以编译的)我希望这篇评论能帮助你了解这本书包含什么内容以及它是否值得购买。

此外

您还可以查看有关此主题的以下资源:
  1. EdX - Java 编程简介:基本数据结构和算法
  2. LinkedIn - Java 数据结构和算法简介(付费)
  3. 27 个提供谜题的网站,可提高您的编程技能
  4. Java 编码蝙蝠
  5. 程序员的任务,不同复杂性任务的答案
#维亚切斯拉夫
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION