阅读 46

Arts 第十一周(5/27 ~ 6/2)

ARTS是什么?
Algorithm:每周至少做一个leetcode的算法题;
Review:阅读并点评至少一篇英文技术文章;
Tip:学习至少一个技术技巧;
Share:分享一篇有观点和思考的技术文章。


Algorithm

LeetCode 132. Palindrome Partitioning II

思路分析

给定一个字符串,把它拆分成回文字符串,假设一分为二(切一刀)为一次,问最少要拆分多少次。看到最小这个关键词,然后再看这个问题,发现原问题是可以分解成更小的子问题的,比如一刀切成两个字符串,这两个字符串就是原问题的子问题,且 原问题的解 = 子问题1的解 + 子问题2的解 + 1,因此首先想到的就是动态规划,但是这里困扰我蛮久的是状态的定义和递推方程的推导。一开始我定义的动态规划数组只有一维,dp[i] 表示问题 [0, i] 的解,我感觉这样的状态定义简单,而且递推方程也特别的好写,即 dp[i] = dp[j] + 1,这里的 j 是在 [0,i) 之间的一个数,我们只需要保证 [j, i] 是回文串,在遍历的过程中,已经记录了 [0, j] 的最优解,结果记录在 dp[j] 中,于是我写出了下面的代码


参考代码(优化前)

public int minCut(String s) {
    if (s == null || s.length() <= 1) {
        return 0;
    }
    
    int n = s.length();
    char[] sArr = s.toCharArray();
    
    int[] dp = new int[n];
    Arrays.fill(dp, Integer.MAX_VALUE);
    
    dp[0] = 0;
    for (int i = 1; i < n; ++i) {
        if (validation(sArr, 0, i)) {
            dp[i] = 0;
            continue;
        }
        
        for (int j = 0; j < i; ++j) {
            if (dp[j] != Integer.MAX_VALUE && validation(sArr, j + 1, i)) {
                dp[i] = Math.min(dp[i], dp[j] + 1);
            }
        }
    }
    
    
    return dp[n - 1];
}

private boolean validation(char[] sArr, int i, int j) {
    while (true) {
        if (i >= j) {
            break;
        }
        
        if (sArr[i++] != sArr[j--]) {
            return false;
        }
    }
    
    return true;
}
复制代码

优化思路

上面给出的解是可以 work 的,但是你可以看到的是这里重复不断地去判断一个子串是不是回文串花费了大量的时间,直接一点的优化思路就是在这个上面做文章,用一个二维的 boolean 数组来储存字符串的每个区间是不是回文串,但是这样做还是免不了去进行大量的判断回文串的工作。如果需要进一步优化,仔细看看回文串,其有个特点就是 str1 = c1 + str2 + c2,如果要保证这里的 str1 是回文串,需有两个条件,str2 是回文串,而且头尾两个字符必须相等,即 c1 = c2,这里的子问题是 str2,遍历的方法还是和上面不变,但是现在我们需要一个二维的数组用来记录回文串的情况,更新与遍历同步进行,不需要反复地判断一个字符串是不是回文串,这样就利用空间换时间的方式,大大提升了程序运行时的效率,代码如下


参考代码(优化后)

public int minCut(String s) {
    if (s == null || s.length() <= 1) {
        return 0;
    }
    
    int n = s.length();
    char[] sArr = s.toCharArray();
    
    boolean[][] isPalindrom = new boolean[n][n];
    int[] dp = new int[n];
    
    for (int i = 0; i < n; ++i) {
        int min = Integer.MAX_VALUE;
        for (int j = 0; j <= i; ++j) {
            if ((sArr[j] == sArr[i]) && (j + 1 >= i - 1 || isPalindrom[j + 1][i - 1])) {
                isPalindrom[j][i] = true;
                
                if (j == 0) {
                    min = 0;
                } else {
                    min = Math.min(min, dp[j - 1] + 1);
                }
            }
        }
        
        dp[i] = min;
    }
    
    return dp[n - 1];
}
复制代码


Review

两篇关于高效学习编程文章,其中第二篇文章是第一篇的一个引用:

The Key To Accelerating Your Coding Skills

Smart Google Search

第一篇主文章中给出了一个观点就是,高效学习编程主要在于渡过 “转折点”,什么是 “转折点” ?作者解释学习编程其实是有两个阶段,第一个阶段是模仿以及积累知识,在这个阶段我们主要看重的是区域性的知识的积累和学习,比如如何在一门不熟悉的语言中写 for 循环,新建函数、对象,访问数据库,调用恰当的库函数等等,这个阶段我们主要是跟着一些教程来做项目,在这个过程中我们会熟悉并掌握一些基本的技能。到了第二个阶段,我们则需要用自己的能力去解决一个问题,完成一个项目,这时我们没有可以参照的教程以及攻略文档,这时我们的心态跟第一阶段的时候发生了很大的变化,第一阶段中,我们明确知道手头的项目是肯定可以完成的,因为我们有教程,有答案,我们心里没有任何的顾虑;之前说的 “转折点” 就是指第一阶段过渡到第二个阶段的那个时期,在此之中,我们不再像之前那样,有一个明确的方向,我们常常需要摸着石头过河,试着解决自己从来没有遇到过的问题,这时你会感觉很痛苦,原因文章也说了,因为在 “转折点” 中,你的解决问题的速度会相比第一个阶段更慢,在第二阶段我们学的不再是积累技能和知识点,而是过程性的学习,说的更直白些就是如何将之前学到的知识点串起来解决实际的问题,由点到线,这其实是一个思维维度的跳跃,难,是正常的。

作者同时也给出了如何快速渡过 “转折点” 的建议,首先就是不要放弃,要知道学习编程的人都有这么一个过程,现在的困难只是暂时的,不是永久的;另外就是学习看技术文档,而不是反复看手把手的教程。他同时指出,学习网络开发其实是有两个转折点,这两个转折点会同时到来,一个是对 web 框架的初步掌握,到理解,再到熟练运用;另外一个是算法和数据结构的学习,我们必须学习一些常见的算法和数据结构,这样才能使我们的思考更加的高效和优化。

在第二篇文章中,作者给了一个关于如何高效解决程序中出现的问题,以及如何提升这方面的能力。程序中出问题,对于编程新手或者编程老手都是再正常不过的,但是新手和老手的心态完全是不一样的,新手会觉得自己又遇到难题了,很心烦,而老手则会思考问题的方方面面,如何快速解决这个问题,解决问题其实就是简单的两个步骤

  1. 定位并找到问题的所在
  2. 寻找恰当的解决方案

很多人,包括我很多时候第一个步骤没有做或者是没有做完就开始执行第二个步骤,这其实并不是高效的方法,解决问题的难点永远在于准确的定位到问题,这就好像你的钱包掉了,你首先得清楚地想想是在哪掉的,如果你定位的范围比较小,那么找起来就会非常轻松。当然,作者也说到这其实是一个过程,一开始我们遇到问题,去 Google 上面搜索解决问题的解的时候可能需要打开 10 个页面,但随着我们对语言、框架等一些东西的了解,我们遇到问题就会很快定位,这时我们再去 Google 上面搜索可能只需要打开 2 个页面。

第一篇文章的最后,作者也说了,编程就是一个持续学习的过程,我们学习并掌握了一个技术的方方面面,这时我们又会去学习下一个技术,接触自己从未接触过的领域,接受下一个更难的挑战,我们每到一个舒适区就要想着如何跳出去,寻找下一个挑战,持续学习才是如何学好编程的最好答案。



Tip

这次分享一些字符编码的知识,之前看到众多的字符编码,不知道它们之间的关系,以及为什么会有这种编码?它们是怎么演变而来的?它们解决了什么问题?希望以后遇到字符编码问题,至少心中不再不知所措,首先,我们来了解几个关键词:

  • 字符集(Character Set):已编号的字符的有序集合,比如 ASCII
  • 字符码(Code Point):字符集中字符的数字编号,例如字母 'A' 在 ASCII 码中的字符码就是 65
  • 编码:将字符转化成字节流
  • 解码:将字节流转化成字符
  • 字符编码(Character Encoding):将字符集中的编码映射为字节流的实现方案,例如最简单的对于最简单的 ASCII 编码,例如字符 'A' 用这种编码方案,表示就是 0x41,写入设备就是 b'01000001'
  • 大小端:计算机传输多字节的时候,先传高位字节还是低位字节有着不一样的看法,但是写是由字节流从低向高写,如果先写入高字节就是大端模式,反之则是小端模式,大端模式字节序和流设备的地址顺序是相反的,而小端模式则是相同,一般网络协议都是采用大端模式进行传输,操作系统都是小端

下面是我画的一张图,算是一个小型知识地图吧,可以大概描述字符编码的演变,以及相关编码方法的由来,可以先看看,图中的文字描述是指遇到的问题

计算机是只能识别数字的,要让计算机识别字符和文字,我们需要把字符和文字转换为数字,于是有了字符编码的存在。最开始,科学家门发明了最简单的字符编码,ASCII 码,这种编码方式只涵盖了英文大小写字母以及一些常用的符号,还有一些控制符号。这里我们使用一一对应的方式来查表进行字符的编码和解码,这里必须强调的是字符和数字必须一一对应,否则会出现同样的字符在计算机上显示出来的东西不一样。另外有一点就是,ASCII 码是单字节的编码方式,也就是只有 8 个 bits 用来表示单个字符,但是这里的 ASCII 码仅使用了非符号位,也就是 7 个 bits 来表示,因此最多只能表示 127 个字符,剩下的一个 bit 用作通信的奇偶校验。

后来人们发现很多很常用的字符 ASCII 码没有涵盖,于是打起了最高位的那个 bit 的注意,OEM 字符集是对 ASCII 的扩展,把最高位也考虑进来,于是 ASCII 扩展成了可以表示 256 个字符,但是 OEM 有很多个版本,对于这些版本有区别的是在 128~256 表示的字符,对于前面的 127 个字符是完全兼容 ASCII 码的。

随着计算机的普及,很多非拉丁语系的国家也开始使用计算机,比如中国,韩国,日本,这么看来使用 ASCII 的单字节编码的方式是肯定行不通了,于是出现了双字节和多字节编码方式,图中的 GBKGB2312 就是双字节编码,但是编码的方式还是查表,表的大小还是 256,但是现在不止一张表了,有多张表,两个字节,第一个字节用来表示字符在第几张表中,第二个字节表示字符在该表中具体所在的位置(类似于行和列的关系)。你可以发现在这个时候出现了很多不同的字符集,如果一个计算机想要解码一个文档就得安装与这个文档匹配的字符集。

多字节编码的方式虽然是解决了很多问题,但是现在的问题是很多内容不能出现在同一个文档中,一份日文的文档就得用日文的字符集,一份中文的文档就要用类似 GBK,GB2312 这样的字符集,如果说一份文档既有日文,又有中文,那么该如何解决?也就是说我们还是没有一个可以表示所有字符的字符集。于是这个时候有了 Unicode 字符集,它将所有的字符按频繁程度划分成了 17 个层面,每个层面有 2 个字节的空间,我们最常用的层面是 BMP 层面,基本涵盖了世界上所有的字符,注意这里说的 Unicode 只是字符集,它不是编码方法,因为使用之前的查表的方式来编码解码会使字符和字节流的耦合度过高,不利于以后的扩展,虽然每个字符在 Unicode 表中都可以找到唯一对应的编码(Unicode 码),但是决定最终的字节流的还是具体的字符编码。对于这里基于 Unicode 的字符编码,一开始的话是 UCS-2,它也是双字节的编码方式,但是其只考虑了 BMP 层面的字符,对于 Unicode 的其他层面的字符没法表示,于是有了 UTF-16,这里会使用 2~4 个字节来表示具体字符,也就是最少都要使用 2 个字节,但是这里会有一些效率问题,我们经常使用的 ASCII 码中的东西如果用 2 个字节表示,最高位都是 0x00,这是非常没有必要的,那么于是就有了 UTF-8,其采用 1~4 个字节来编码,这种编码方式对于 ASCII 码中的内容用单字节传输,大大节省了编码效率,另外的话也解决了一些不兼容问题,比如 C 语言中的函数都将 0x00 视为字符串的末尾。这里还有一个编码方式是我国的 GB18030,因为它也可以将 Unicode 里面的所有字符都转化成字节流,所有也是 Unicode 编码方式,但是这里不同的是,它还是采用查表的方式来进行编码,区别于 UTF-8 和 UTF-16 的规则型编码。



Share

这周看完了一本书,《简单的逻辑学》,读之前觉得逻辑学就是那些哲学家搞的东西,但是这本书从基本的逻辑学论证开始讲起,很基础,小白都能读的懂,读完之后发现逻辑在我们工作以及生活中无处不在,学会用逻辑去思考有助于我们得知事情的真相,减少不必要的焦虑,这次就来写个读书感悟吧

程序员需要了解的逻辑学思想

关注下面的标签,发现更多相似文章
评论