肝了几万字,送给看了《算法图解》却是主攻Java的你和我(上篇)

生活总是这样,不能叫人处处都满意,但我们还要热情的活下去。

楔子

最近看了下《算法图解》这本书,写的确实通俗易懂、生动有趣,很适合算法入门(作者快打钱 !!!)。如果有需要的话,本博主也贴心的准备了这本书的电子版,通过下图下方的百度网盘链接就可下载哦!

在这里插入图片描述

链接:https://pan.baidu.com/s/1ubClKcrgOTqqfS7BXJrB8Q
提取码:sywh

但可惜书中示例都是用Python代码实现的,虽然狠狠的感受了一把Python的简洁和优雅之处,但是对于主攻Java语言的程序员包括我有点不太友好。为此,我特意将书中的Python代码翻译成了Java代码,希望对大家能有帮助。

因此本篇博客结合《算法图解》一书食用更佳!当然,本博主并不是简单粗暴的直接翻译,而是辅以必要的讲解,所以没有看过《算法图解》一书也可放心食用。

第1章 算法简介

1.2 二分查找

二分查找很像我们之前酒桌喝酒玩的猜数字的游戏。当时我们为了增加游戏的激烈程度,往往猜的数字都是直接对半砍。

方法binary_search用于接收一个有序数组和一个元素。如果指定的元素包含在数组中,这个方法将返回其位置。你将跟踪要在其中查找的数组部分——开始时为整个数组。

int low = 0;
int high = array.length - 1;

在这里插入图片描述

你每次都检查中间的元素:

// 整数除法,如果(low + high)不是偶数,Java自动将mid向下取整
int mid = (low + high) /2; // (0+3)/2 = 1(1.5向下取整)
int guess = array[mid];

如果猜的数字小了,就相应的修改low值:

if (guess < item)
	low = mid + 1;

如果猜的数字大了,就相应的修改high值:

if (guess > item)
	high = mid - 1;

在这里插入图片描述
完整的代码示例如下:

	/**
     * 用二分查找判断一个数是否存在一个数组里
     * @param array 数组
     * @param item  需要判断数组中是否存在的一个指定元素
     * @return
     */
    private static int binary_search(int[] array, int item) {
        //需要查找的最小范围 - 数组索引从0开始
        int low = 0 ;
        // 需要查找的最大范围 - 数组长度-1
        int high = array.length - 1;

        while (low <= high) { // 只要范围没有缩小到只包含一个元素,就继续查找
            // 整数除法,如果(low + high)不是偶数,Java自动将mid向下取整
            int mid = (low + high) /2; // 如 (0+3)/2 = 1(1.5向下取整)
            int guess = array[mid]; // 检查中间的元素

            if (item == guess) // 如果找到了这个元素,则返回这个元素的索引即位置
                return mid;
            if (guess > item)   // 猜的数字大了
                high = mid - 1; // 不要使用 middle--
            else                // 猜的数字小了
                low = mid + 1; // 不要使用 middle++
        }

        return -1; // 返回-1表示没有指定的元素
    }

先写一个方便显示二分查找结果的方法:

	/**
     * 根据二分查找的结果打印出相应的内容
     * @param array 用于查找的数组
     * @param index 二分查找返回的索引
     * @param item  查找的元素
     */
    private static void printResult(int[] array, int index, int item) {
        if (index == -1)
            System.out.println("很抱歉,数组 "+ Arrays.toString(array) +" 中没有 " + item + " 这个元素");
        else
            System.out.println("数组 "+ Arrays.toString(array) +" 中存在 " + item + " 这个元素,它的索引为:" + index);
    }

来测试一下吧:

	public static void main(String[] args) {
        // 声明一个数组
        int[] array = {1, 3, 5, 7, 9};

        System.out.println("=================================================");
        // 需要查找的元素
        int item = 7;
        // 进行二分查找并返回相应的结果
        int index = binary_search(array, item);
        // 根据二分查找的结果打印出相应的内容
        printResult(array, index, item);

        // 需要查找的元素
        int item1 = -1;
        // 进行二分查找并返回相应的结果
        int index1 = binary_search(array, item1);
        // 根据二分查找的结果打印出相应的内容
        printResult(array, index1, item1);
        System.out.println("=================================================");
    }

运行main方法:
在这里插入图片描述
完美!!!

第2章 选择排序

2.3 选择排序

选择排序的速度虽然没有快速排序这么快,但也是一种很灵巧的算法。如以下代码可通过选择排序实现:将数组元素按从小到大的顺序排列。 我们先编写一个用于找出数组中最小元素的方法:

	/**
     * 找出一个数组中最小的元素,并返回它的索引
     * @param arr
     * @return
     */
    private static int findSmallest(int[] arr) {
        // 最小元素的值,默认第一个元素为最小的元素
        int smallest = arr[0];
        // 最小元素的索引,默认第一个元素为最小的元素
        int smallestIndex = 0;

        // 遍历数组 - 因为默认第一个元素为最小元素,所以从1开始
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] < smallest) {
                smallest = arr[i];
                smallestIndex = i;
            }
        }

        return smallestIndex;
    }

现在可以使用这个方法来编写选择排序了:

	/**
     * 选择排序,返回一个排序后的数组 -> 由小到大
     * @param arr
     * @return
     */
    private static int[] selectionSort(int[] arr) {
        int[] newArr = new int[arr.length];

        //遍历需要排序的数组
        for (int i = 0; i < arr.length; i++) {
            // 找出数组中最小的元素的索引
            int smallestIndex = findSmallest(arr);
            // 得到这个最小的元素
            int smallest = arr[smallestIndex];
            // 将目前数组中最小的元素添加到新的数组中
            newArr[i] = smallest;
            // 将这个最小的元素从旧的数组中弹出,以便下次循环
            arr.pop(smallestIndex);
        }

        return newArr;
    }

再来测试一下:

	public static void main(String[] args) {
        // 定义需要排序的数组
        int[] arr = {5, 3, 6, 2, 10};
        // 用选择排序对数组进行排序,并返回一个排序好的数组
        int[] newArr = selectionSort(arr);
        // 输出排序好的数组
        System.out.println(Arrays.toString(newArr));
    }

逻辑好像很完美,那么能得到我们想要的结果吗?答案是不能,因为以上代码编译都通过不了!!!

那么,为什么代码编译通过不了呢?

因为Java中没有pop这个方法,即Java不能通过数组的索引直接弹出数组中对应的元素!!!

// 将这个最小的元素从旧的数组中弹出,以便下次循环
arr.pop(smallestIndex);

这个时候有些学习Java语言的小伙伴就开始慌了,那难道Java语言就实现不了选择排序吗?不会吧!!!

在这里插入图片描述

不要慌,问题不大!本博主就是为了解决此类问题而来。

用Java来实现选择排序的话,同样的我们需要找到数组中的最小元素,但是我们并不需要一个新的数组来一个一个装值!!! 不过,我们可以将需要排序的数组看成两个数组,左边的数组是已经排好序的数组,右边的数组是乱序的数组。当左边的排好序的数组越来越长,最后吞噬掉右边乱序的数组的时候,这个数组就排好序了。这样讲可能会有点让人云里雾里。

不过没关系,下面直接上步骤,上面文字的个中滋味你就能体会到了。

首先,我们假定数组的第一位为最小元素:
在这里插入图片描述
然后,我们从假定最小元素的位置向后遍历数组进行比较:
在这里插入图片描述
这样我们就可以求出目前数组中最小的值,也就是图中位于索引4处的0元素:
在这里插入图片描述
然后我们再把现在求出的最小元素和之前假定的最小元素互换位置:
在这里插入图片描述
我们把元素的位置互换之后,再假定数组的第二个位置也就是之前假定位置的下一位为最小元素:
在这里插入图片描述
然后再重复类似于之前的操作:

  1. 向后遍历数组,得出从假定最小元素位置后开始数组的最小值。
  2. 数组中假定的最小元素和求出的最小元素互换位置
  3. 再假定之前假定位置的下一位为最小元素,继续向后遍历比较,互换位置。

这样我们就可以发现似乎左边有一个有序的数组,右边有一个乱序的数组,而右边乱序的数组一直在抛出最小元素给左边有序的数组吞噬。当右边的数组被左边的数组吞噬完了,左边的数组就成功的进化成了完整版的有序数组。这样我们的排序就完成了。听起来好像有点血腥暴力

好,原理解释完了,直接上代码!!

	/**
     * 以最小元素为标志位 -> 选择排序(升序)
     * @param arr 需要排序的数组
     * @return
     */
    private static int[] selectionSort(int[] arr) {

        // 默认最小值为第一个元素值
        int smallest = arr[0];
        // 最小值的索引
        int index = 0;

        // 外层循环控制需要比较的次数
        for (int i = 0; i < arr.length - 1; i++) {

            // 遍历数组,开始比较 {5, 3, 6, 2, 0}
            for (int j = i + 1; j < arr.length; j++) {
                // 判断当前遍历的元素是否比最小的元素小,直至求出当前乱序(右边)数组中的最小值
                if (arr[j] < smallest) {
                    smallest = arr[j];
                    index = j;
                }
            }

            // 之前假定的最小元素和求出的最小元素互换位置
            int temp = arr[i];
            arr[i] = smallest;
            arr[index] = temp; 
            
            // 假定的最小元素向后移动一位,以便下次比较
            smallest = arr[i+1];
            index = i+1;
        }

        return arr;
    }

来测试一下吧:

public static void main(String[] args) {
        // 需要排序的数组
        int[] arr = {5, 3, 6, 2, 0};
        // 进行选择排序,并返回一个已经排序好的数组
        selectionSort(arr);
        // 输出已经排序好的数组
        System.out.println(Arrays.toString(arr));
}

运行main方法:
在这里插入图片描述
排序成功,大功告成!!!

第3章 递归

个人一直认为,编程是一门艺术。而递归则是这门艺术中的优雅之王!

没错,说的就是本帝龟了!!!
在这里插入图片描述
在《算法图解》第三章《递归》中,作者把人分为三种:

  1. 恨它的。
  2. 爱它的。
  3. 恨了几年后又爱上它的。

作者坦言承认自己属于第三种,而本博主则是第一眼就爱上了递归,并且与递归结下了不解之缘。以下有博客为证!

  1. 递归和循环之间不得不说的故事
  2. 递归实战篇:查询商品分类的实现
  3. 今天我们来聊聊递归喝汽水问题

可以不为夸张的说,我一半以上博客访问量都是靠的递归。而《算法图解》中对于算法和循环的观点跟我之前的博客中总结的观点不谋而合。在此引用Leight Caldwell在Stack Overflow上说的一句话:

如果使用循环,程序的性能可能会更高;如果使用递归,程序可能更容易理解。

而如果想深入理解这句话的话,可以点击上面的链接去看看我的递归系列博客。包括关于递归的终止条件也就是本书中的基线条件,以及的概念在我的递归系列博客中已经讲的很清楚了,此处就不再多加赘述。

第4章 快速排序

4.1 分而治之

给定一个数字数组:
在这里插入图片描述
使用循环将这些数字相加,并返回结果:

/**
 * @author guqueyue
 * @Date 2020/7/24
 * for循环求出数组各元素之和
 **/
public class ForArraySum {
    public static void main(String[] args) {
        // 声明一个数组并赋值
        int[] arr = {2, 4, 6};
        // 求数组各元素之和并返回这个和的值
        int total = sum(arr);
        // 输出结果
        System.out.println(Arrays.toString(arr) + "中各元素之和为:" + total);
    }

    /**
     * 使用for循环将数组中的数字相加并返回结果
     * @param arr
     * @return
     */
    private static int sum(int[] arr) {
        int total = 0;

        // 遍历数组
        for (int x : arr) {
            total += x;
        }

        return total;
    }
}

运行得:
在这里插入图片描述
使用递归求上述数字数组的和:

/**
 * @author guqueyue
 * @Date 2020/7/8
 * 递归计算数组的和
 * index - 1 和 index--
 **/
public class RecursionArraySum {
    public static void main(String[] args) {
        int[] arr = {2, 4, 6};
        // 用递归求数组之和
        int sum = recursion(arr, 0);
        System.out.println(Arrays.toString(arr) + "中各数字之和为:" + sum);
    }
	
	/**
     * 用递归求数组之和,从第一个数加起
     * @param arr 数组
     * @param index 第一个元素的索引
     * @return
     */
    private static int recursion(int[] arr, int index) {
        
        if (index == (arr.length - 1)) {
            return arr[index];
        }else {
            return arr[index] + recursion(arr, index+1);
        }
    }
}

运行得:
在这里插入图片描述

4.2 快速排序

快速排序是一种很常见的排序算法,最近我在工作中也用到了这种排序算法那。在排序速度方面,它比我们之前介绍的选择排序要快得多。

在《算法图解》一书中,作者已经把快速排序中蕴含的分而治之的思想策略说的很清楚了。在此,我就不多加赘述。毕竟这篇也不是专门介绍某种算法的博客,而是类似于《算法图解》的学习笔记。当然,我以后也会出关于各种算法介绍的博客。有感兴趣的小伙伴可以关注我哦!加关注,不迷路!!!

当然,看到这里但是却没有看过这本书的小伙伴可以去到本篇博客开始的地方找到百度网盘的链接下载即可(不过,我猜是没有人能看到这里的

如果我们仿造书中作者用Python的代码示例中的逻辑,写出来的Java代码应该是这样的:

/**
 * @author guqueyue
 * @Date 2020/7/27
 * 模仿Python代码示例的快速排序
 **/
public class QuickSortFromPython {

    public static void main(String[] args) {
        // 声明一个数组
        int[] array = {10, 5, 2, 3, 10};
        // 获得排序成功的数组
        int[] newArray = quickSort(array);
        // 打印排好序的数组
        System.out.println(Arrays.toString(newArray));
    }

    /**
     * 快速排序
     * @param array 需要排序的数组
     * @return
     */
    private static int[] quickSort(int[] array) {

        if (array.length < 2) { // 基线条件:为空或只包含一个元素的数组是“有序”的

            return array;
        } else { // 递归条件
            // 取数组第一个元素为基准值
            int pivot = array[0];

            // 由所有小于等于基准值的元素组成的子数组(去除基准值)
            int[] less = getSmallArray(array, pivot);

            // 由所有大于基准值的元素组成的子数组(去除基准值)
            int[] greater = getBigArray(array, pivot);

            return getSortArray(quickSort(less), pivot, quickSort(greater));
        }
    }

    /**
     * 根据传入的小于基准值的数组、基准值以及大于基准值的数组
     * 拼接成一个数组
     * @param less 小于基准值的数组
     * @param pivot 基准值
     * @param greater 大于基准值的数组
     * @return
     */
    private static int[] getSortArray(int[] less, int pivot, int[] greater) {

        // 声明一个新数组
        int[] newArray = new int[less.length + 1 + greater.length];

        // 遍历小于基准值的数组, 赋值给新数组
        for (int i = 0; i < less.length; i++) {
            newArray[i] = less[i];
        }

        // 添加基准值
        newArray[less.length] = pivot;

        // 遍历大于基准值的数组,赋值给新数组
        for (int i = 0; i < greater.length; i++) {
            newArray[i + less.length + 1] = greater[i];
        }

        return newArray;
    }

    /**
     * 根据传入的数组和基准值,返回小于等于这个基准值的数组
     * @param array 数组
     * @param pivot 基准值
     * @return
     */
    private static int[] getSmallArray(int[] array, int pivot) {

        // 声明一个集合用来装值
        List<Integer> list = new ArrayList<Integer>();

        // 遍历数组
        for (int i = 1; i < array.length; i++) {
            if (array[i] <= pivot) {
                list.add(array[i]);
            }
        }

        // 将集合转换成数组
        int[] smallArray = new int[list.size()];
        for (int i = 0; i < smallArray.length; i++) {
            smallArray[i] = list.get(i);
        }

        return smallArray;
    }

    /**
     * 根据传入的数组和基准值,返回大于这个基准值的数组
     * @param array 数组
     * @param pivot 基准值
     * @return
     */
    private static int[] getBigArray(int[] array, int pivot) {

        // 声明一个集合用来装值
        List<Integer> list = new ArrayList<Integer>();

        // 遍历数组
        for (int i = 1; i < array.length; i++) {
            if (array[i] > pivot) {
                list.add(array[i]);
            }
        }

        // 将集合转换成数组
        int[] bigArray = new int[list.size()];
        for (int i = 0; i < bigArray.length; i++) {
            bigArray[i] = list.get(i);
        }

        return bigArray;
    }
}

运行main方法:
在这里插入图片描述
排序成功!可是真的完美吗?书中用Python代码十几行实现的代码,仿照它的逻辑用Java写却用了一百多行!!!

那么,我又要发出灵魂的拷问了:Java有这么弱吗?

当然没有!!!Java和Python作为两种不同的编程语言。两者由于语法的不同,自然会有不同的实现方式。同选择排序有些类似的是,Java实现快速排序也并不需要得到什么新的数组,直接在一个数组内进行排序操作即可:

/**
 * @author guqueyue
 * @Date 2020/7/8
 * 快速排序
 **/
public class QuickSort {

    static int[] arr = {8, 2, 17, 5, 8, 12};

    /**
     * 快速排序 -> 升序排序
     * @param array
     * @param begin
     * @param end
     */
    private static void quickSort(int[] array, int begin, int end) {
        int i, j, flag, temp;
        if (begin > end) {
            return;
        }
        i = begin;
        j = end;
        //基准值
        flag = array[begin];

        while (i < j) {
            // 从右往左找, 直到找到一个比基准值小的元素
            for (; array[j] >= flag && i < j; j--);

            // 从左往右找, 直到找到一个比基准值大的元素
            for (; array[i] <= flag && i < j; i++);

            // 如果满足条件就交换位置
            if (i < j) {
                temp = array[j];
                array[j] = array[i];
                array[i] = temp;
            }
        }

        // 最后将基准值与 i和j相等位置即最后i、j相遇位置的数字交换
        // flag = array[begin]; -> 之前的代码
        array[begin] = array[j];
        array[j] = flag;

        // 递归调用左半数组
        quickSort(array, begin, j-1);

        // 递归调用右半数组
        quickSort(array, j+1, end);
    }

    public static void main(String[] args) {
        quickSort(arr, 0, arr.length-1);
        System.out.println(Arrays.toString(arr));
    }
}

运行main方法,得:
在这里插入图片描述
完美!虽然代码量还是比python多,但也没多多少。况且Java优势在于企业级开发,也不是算法这方面,所以也不用纠结这个问题了。

以上代码关于两个数互换问题如果不懂的,可以看我的这篇博客:

实现两个数互换的八种方法

至于关于用Java实现快速排序的具体步骤讲解。
以后本博主会出算法系列的博客进行详细详解的,欢迎大家关注我哦!

第5章 散列表

5.1 散列函数

用Java语言来创建散列表:

/**
 * @author guqueyue
 * @Date 2020/7/28
 * 散列表
 **/
public class HashTable {
    public static void main(String[] args) {
        // 创建散列表book
        HashMap<String, Double> book = new HashMap<String, Double>();
        // 创建散列表book之后,在其中添加一些商品的价格
        book.put("apple", 0.67); // 一个苹果的价格为67美分
        book.put("milk", 1.49);  // 牛奶的价格为1.49美元
        book.put("avocado", 1.49);

        // 输出book
        System.out.println(book);

        // 得到鳄梨的价格
        System.out.println("鳄梨的价格为:" + book.get("avocado") + "美元");
    }
}

运行应用程序,得:
在这里插入图片描述

5.2 应用案例

5.2.1 将散列表用于查找

用Java语言来创建电话薄:

/**
 * @author guqueyue
 * @Date 2020/7/28
 * 电话薄
 **/
public class PhoneBook {
    public static void main(String[] args) {
        // 创建一个散列表
        HashMap<String, String> phoneBook = new HashMap<String, String>();

        // 添加一些联系人的电话号码
        phoneBook.put("jenny", "8675309");
        phoneBook.put("emergency", "911");

        // 查找jenny的电话号码
        String name = "jenny";
        System.out.println(name + "的号码电话为:" + phoneBook.get(name));
    }
}

运行应用程序,控制台输出如下:
在这里插入图片描述
查找成功!!!

5.2.2 防止重复

避免重复投票:

/**
 * @author guqueyue
 * @Date 2020/7/9
 * 不要把map放在方法里循环new
 **/
public class CheckVoter {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        // 用来存储已经投过票姓名的散列表
        HashMap<String, Boolean> nameMap = new HashMap<String, Boolean>();

        while (true) {
            System.out.print("请输入姓名:");
            String name = scanner.next();
            checkVoter(name, nameMap);
        }
    }

    /**
     * 根据是否已经投过票来输出结果
     * @param name    已经投过票者的姓名
     * @param nameMap 用来存储已经投过票姓名的散列表
     */
    private static void checkVoter(String name, HashMap<String, Boolean> nameMap) {
		
        if (nameMap.containsKey(name)) { // 判断是否已经投过票
            System.out.println("kick them out!");
        }else {
            nameMap.put(name, true); // 投过票的把他存起来
            System.out.println("let them vote!");
        }
    }
}

运行应用程序:
在这里插入图片描述
完美!!!

5.2.3 将散列表用作缓存

说到缓存,可能广大的程序员朋友们对这个词语并不陌生。往往代码的逻辑明明并没有错,已经检查了千百遍,却就是出不了想要的效果,结果发现是缓存的锅,气的直想砸电脑。
在这里插入图片描述
又或者,发现别人写的代码有问题,明明已经对环境进行了各种检查,清了千百遍缓存。然后去问别人的时候,别人张口就来一句:你缓存清了没有?然后就没理你了。
在这里插入图片描述
但是作为一个程序员,从设计的角度来讲,不得不承认,缓存是个好东西。缓存就是让网站将数据记住,而不再重新计算,这极大的减轻了服务器的压力,特别是在高并发的情况下。

当然了,其实散列表用作缓存的逻辑跟之前防止重复例子的逻辑差不多:

/**
 * @author guqueyue
 * @Date 2020/7/28
 * 缓存模拟
 **/
public class Cache {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);

        Map<String, String> cache = new HashMap<String, String>();

        while (true) {
            System.out.print("请输入网址:");
            String url = input.next();

            // 根据网址返回页面
            String page = getPage(url, cache);
            // 模拟返回的页面,并打印出来
            System.out.println("服务器返回了:" + page);
        }
    }

    /**
     * 根据url得到页面
     * @param url
     * @param cache
     * @return
     */
    private static String getPage(String url, Map<String, String> cache) {

        if (cache.containsKey(url)) { // 判断缓存中是否有这个网址对应的页面
            // 如果有,直接从缓存中返回页面
            return cache.get(url);
        }else {
            // 如果缓存中没有有这个网址对应的页面,则去向服务器请求数据再生成这个页面
            String dataPage = getDataFromServer(url);
            // 再将生成这个页面所需的数据也就是这个页面放入缓存中
            cache.put(url, dataPage);
            return dataPage;
        }
    }

    /**
     * 模拟服务器根据url返回相应的数据以便生成页面
     * @param url
     * @return
     */
    private static String getDataFromServer(String url) {
        System.out.println("服务器接收到了url请求...");

        // 模拟生成的页面
        String dataPage = "一个" + url + "的页面";
        // 返回这个页面
        return dataPage;
    }
}

运行应用程序:
在这里插入图片描述
我们可以很清晰的看到:当我们第一次发出百度和淘宝的请求时,服务器都接收到了url请求,也就是说从服务器返回了用于生成页面的数据。但是当我们第二次发出百度的请求时,浏览器并没有接收到请求,也就是说这个时候百度主页面是直接在缓存里面取的。

5.2.4 小结

所以,通过以上分析,我们可以得出散列表的三个作用:

  1. 模拟映射关系,就像之前的电话薄以及水果店价格表。
  2. 防止重复, 好比之前的防止重复投票。
  3. 缓存/记住数据,以免服务器再次通过处理来生成它们。

第6章 广度优先搜索

广度优先搜索(breadth-first search, BFS)是为了解决最短路径问题所产生的算法,如计算出前往朋友家的最短路径,又或者求出象棋中把对方将死的最少步数。广度优先搜索作为一种用于图的查找算法,可帮助回答两类问题:

  1. 从一个节点出发,有前往另一个节点的路径吗?
  2. 从一个节点出发,前往另一节点的哪条路径更短?

6.4 实现图

接下来使用散列表来实现图,图又由多个节点组成,如下图:
在这里插入图片描述
上图我们可以运用以下Java代码来建立映射关系:

 Map<String, String[]> graph = new HashMap<String, String[]>();
 graph.put("you", new String[]{"alice", "bob", "claire"});

其实图只不过是一系列的节点和边,哪怕如下图一样复杂的图,我们也能用代码表示。
在这里插入图片描述
上图用Java代码表示如下:

Map<String, String[]> graph = new HashMap<String, String[]>();
graph.put("you", new String[]{"alice", "bob", "claire"});
graph.put("bob", new String[]{"anuj", "peggy"});
graph.put("alice", new String[]{"peggy"});
graph.put("claire", new String[]{"thom", "jonny"});
graph.put("anuj", null);
graph.put("peggy", null);
graph.put("thom", null);
graph.put("jonny", null);

6.5 实现算法

关于算法的实现原理请翻书哦!另外关于队列的概念书中也讲的清楚了,此处我也就不加赘述。
这里我们就像书中一样通过Java代码来实现查找你的人际关系圈中是否有芒果商(这里我们默认名字最后一个字母为m的为芒果商):

/**
 * @author guqueyue
 * @Date 2020/7/30
 * 查找你的朋友是否有芒果商
 **/
public class MangoSeller {
    public static void main(String[] args) {

        // 创建一张用来表示你人际关系的图
        Map<String, String[]> graph = new HashMap<String, String[]>();
        createGraph(graph);

        // 创建一个队列
        List<String> searchQueue = new ArrayList<String>();
        // 将你的邻居都加入到这个搜索队列中
        addSearchQueue(searchQueue, graph.get("you"));

        // 检查你的人脉关系中是否有芒果商
        boolean flag = searchQueue(searchQueue, graph);
        // 如果返回false,则表明你的人脉圈中没有芒果商人
        if (!flag) System.out.println("不好意思,你的朋友中没有芒果商人!!!");
    }

    /**
     * 检查你的人脉关系中是否有芒果商
     * @param searchQueue 队列
     * @param graph 人际关系图
     * @return
     */
    private static boolean searchQueue(List<String> searchQueue, Map<String, String[]> graph) {

        // 只要队列不为空,就一直遍历查找
        while (searchQueue.size() != 0) {
            // 取出其中的第一个人, 并移除
            String person = searchQueue.get(0);
            searchQueue.remove(0);

            // 检查这个人是否是芒果商
            if (personIsSeller(person)) { // 是芒果商

                System.out.println(person + " is a mango seller!");
                return true;
            }else {
                // 不是芒果商,将这个人的朋友都加入到搜索队列中
                addSearchQueue(searchQueue, graph.get(person));
            }
        }

        // 如果最终返回了false,则说明你的人际关系圈中并没有芒果商
        return false;
    }

    /**
     * 通过姓名检查这个人是不是芒果商
     * @param person
     * @return
     */
    private static boolean personIsSeller(String person) {

        // 判断姓名字符串最后一个字符是不是以m结尾
        return person.substring(person.length() - 1).equals("m");
    }

    /**
     * 往搜索队列中添加邻居
     * @param searchQueue
     * @param you
     */
    private static void addSearchQueue(List<String> searchQueue, String[] you) {

        // 如果邻居不为空,则添加
        if (you != null) {
            for (String person : you) {
                searchQueue.add(person);
            }
        }
    }

    /**
     * 人际关系图
     * @param graph
     */
    private static void createGraph(Map<String, String[]> graph) {
        graph.put("you", new String[]{"alice", "bob", "claire"});

        graph.put("bob", new String[]{"anuj", "peggy"});
        graph.put("alice", new String[]{"peggy"});
        graph.put("claire", new String[]{"thom", "jonny"});

        graph.put("anuj", null);
        graph.put("peggy", null);
        graph.put("thom", null);
        graph.put("jonny", null);
    }
}

跑一下代码,得出:
在这里插入图片描述
结果好像很完美,那么有没有什么问题呢?
我们试着来打印一下程序检查的人员以及顺序:
在这里插入图片描述

System.out.println("正在检查:" + person);

运行程序:
在这里插入图片描述
这个时候我们就可以很轻易的发现peggy这个人竟然被检查了两次。也就是说,我们做了重复检查的事情,做了无用功,影响了程序的效率。

这是为什么呢?我们通过之前的人际关系图可以发现原来peggy不但是bob的邻居,他还是alice的邻居,所以他被重复检查了两次:
在这里插入图片描述

我猜这个时候可能就有人会说了:只是影响了一点效率而已,没关系的!

真的没关系吗???

如果这个时候人际关系图是这样的:
在这里插入图片描述
对应的代码:

graph.put("you", new String[]{"peggy"});
graph.put("peggy", new String[]{"you"});

我们试着运行一下程序:
在这里插入图片描述
我们会发现系统陷入了疯狂的死循环中!!!不断的检查你和peggy是否是芒果商。原因很简单,当检查你是否是芒果商的时候,会将peggy加入到搜索队列中;而当检查peggy是否是芒果商时,又会将你加入到搜索队列中。如此周而复始,往复循环!

所以,我们需要在检查的方法中给已经检查过的人做标记,之后不再检查他

代码如下:

   /**
     * 检查你的人脉关系中是否有芒果商
     * @param searchQueue 队列
     * @param graph 人际关系图
     * @return
     */
    private static boolean searchQueue(List<String> searchQueue, Map<String, String[]> graph) {

        // 用于记录检查过的人
        List<String> searched = new ArrayList<String>();

        // 只要队列不为空,就一直遍历查找
        while (searchQueue.size() != 0) {
            // 取出其中的第一个人, 并移除
            String person = searchQueue.get(0);
            searchQueue.remove(0);

            // 只有当这个人没有被检查过才能被检查
            if (!searched.contains(person)) {

                // 检查这个人是否是芒果商
                if (personIsSeller(person)) { // 是芒果商

                    System.out.println(person + " is a mango seller!");
                    return true;
                }else {
                    // 不是芒果商,将这个人的朋友都加入到搜索队列中
                    addSearchQueue(searchQueue, graph.get(person));
                    // 将这个人标记为检查过
                    searched.add(person);
                }
            }
        }

        // 如果最终返回了false,则说明你的人际关系圈中并没有芒果商
        return false;
    }

运行程序:
在这里插入图片描述
完美!!!没有死循环了!

下面的作者的自我吐槽

现在已经是凌晨的2点14分了,不知不觉这篇博客也已经写了2万多字。其实明明知道写这么长的博客不会有很多人看的,明明知道把这篇博客拆分成很多篇访问量会多好多,但是我就是不想这么做。如果有人能够看到这里的话,谢谢你!时候不早了,晚安!
在这里插入图片描述
emmmm,本来初计划是一篇博客写完《算法图解》的Java形式讲解,但是文章太长了,还是分个上下两篇吧。

第一次写博客周期这么长,还没写完。喜欢就点个赞吧!

下篇链接:肝了几万字,送给看了《算法图解》却是主攻Java的你和我(下篇)

  • 23
    点赞
  • 106
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
1.算法是程序的灵魂,优秀的程序在对海量数据处理时,依然保持高速计算,就需要高效的数据结构和算法支撑。2.网上数据结构和算法的课程不少,但存在两个问题:1)授课方式单一,大多是照着代码念一遍,数据结构和算法本身就比较难理解,对基础好的学员来说,还好一点,对基础不好的学生来说,基本上就是听天书了2)说是讲数据结构和算法,但大多是挂羊头卖狗肉,算法讲的很少。 本课程针对上述问题,有针对性的进行了升级 3)授课方式采用图解+算法游戏的方式,让课程生动有趣好理解 4)系统全面的讲解了数据结构和算法, 除常用数据结构和算法外,还包括程序员常用10大算法:二分查找算法(非递归)、分治算法、动态规划算法、KMP算法、贪心算法、普里姆算法、克鲁斯卡尔算法、迪杰斯特拉算法、弗洛伊德算法、马踏棋盘算法。可以解决面试遇到的最短路径、最小生成树、最小连通图、动态规划等问题及衍生出的面试题,让你秒杀其他面试小伙伴3.如果你不想永远都是代码工人,就需要花时间来研究下数据结构和算法。教程内容:本教程是使用Java来讲解数据结构和算法,考虑到数据结构和算法较难,授课采用图解加算法游戏的方式。内容包括: 稀疏数组、单向队列、环形队列、单向链表、双向链表、环形链表、约瑟夫问题、栈、前缀、中缀、后缀表达式、中缀表达式转换为后缀表达式、递归与回溯、迷宫问题、八皇后问题、算法的时间复杂度、冒泡排序、选择排序、插入排序、快速排序、归并排序、希尔排序、基数排序(桶排序)、堆排序、排序速度分析、二分查找、插值查找、斐波那契查找、散列、哈希表、二叉树、二叉树与数组转换、二叉排序树(BST)、AVL树、线索二叉树、赫夫曼树、赫夫曼编码、多路查找树(B树B+树和B*树)、图、图的DFS算法和BFS、程序员常用10大算法、二分查找算法(非递归)、分治算法、动态规划算法、KMP算法、贪心算法、普里姆算法、克鲁斯卡尔算法、迪杰斯特拉算法、弗洛伊德算法马踏棋盘算法。学习目标:通过学习,学员能掌握主流数据结构和算法的实现机制,开阔编程思路,提高优化程序的能力。
### 回答1: 气排球场的初始号位顺序为:发球方的右侧为1号位,左侧为2号位;接球方的左侧为3号位,右侧为4号位。 不同位置上的职责如下: 1号位:发球员,负责发球和组织进攻。 2号位:副攻手,通常是团队的第二进攻点,负责接应和进攻。 3号位:主攻手,团队的第一进攻点,负责进攻和防守。 4号位:接应二传手,负责接应传球和组织进攻,同时也需要有较强的防守能力。 5号位:二传手,团队的组织者,负责传球和组织进攻,同时也需要有一定的防守能力。 6号位:自由人,负责防守和接应,可以在场上自由换位,并且不受换人规则限制。 ### 回答2: 气排球场的初始号位顺序是:发球员在1号位,主攻手在4号位,副攻手在2号位,接应在3号位,二传手在5号位,自由人在6号位。 不同位置上的职责如下: 1号位,发球员:负责开局发球,控制发球力道和方向。在进攻时,也会扮演接应的角色,接二传手的传球进行组织,同时也需要尽快回到自己的位置准备防守。 2号位,副攻手:在进攻中是主要得分点,承担起扣球和拦网的重任。副攻手一般会在中锋位置进行削球,攻击对手的防线,同时也要积极参与拦网,以阻止对方的进攻。 3号位,接应:接应是队伍组织进攻的核心,扮演着二传手的角色。主要负责接住一传并传球给进攻队员,组织并调度球场上的进攻战术。同时,在防守时也要及时调整自己的位置,准备接球。 4号位,主攻手:主攻手是球队的得分手,通常是在边线位置进行扣球。主攻手需要具备高弹跳和强力的扣球能力,同时也要积极参与拦网和防守。 5号位,二传手:二传手是组织进攻的关键人物,负责将一传传到合适的位置给进攻队员。二传手需要具备准确的传球技术和出色的判断和决策能力,能够迅速调整进攻战术。 6号位,自由人:自由人负责守卫后场,接防和接应对手的攻球,同时也是球队的替补出场球员。自由人在进攻时可以替代其他位置的球员上场,为球队提供灵活性和多样化的进攻方式。 以上是气排球场不同位置上的初号位顺序以及各自的职责。队员们在球场上密切配合,发挥各自的优势,共同争取胜利。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值