算法总结

1.    中心思想:
1)    重要:认真审题,尤其是看例子,看清楚再做题
1)    例如一些常见的概念,都会因题目而异,切不可直接跳过熟悉概念的解释,否则极易出错
2)    不同的题目会重新定义已经熟悉的概念,甚至是完全不同;原则应该是:如果题目定义,则严格按照题目要求做题;而如果没有重新定义的概念,应该以算法导论笔记中的内容为主
3)    看清题目的限制条件,某些限制条件会极大地简化输入的可能性,需要极其留意,避免复杂化题目
4)    大多数情况下,应该首先考虑是否可以用递归法解题,之后考虑是否有动态规划的特性
5)    如果使用动态规划的复杂度依然很高,需要考虑其是否有贪婪算法的特性,如果是,则能够大大地简化算法和复杂度,效果相当于动态规划的降阶
2)    法则:递归法(分治算法)和迭代法
1)    两者本质上极其类似,迭代代码量大,但速度快,运算过程中可以清栈,不必保留中间变量;而递归代码量小,速度慢,有可能造成栈溢出,代表运算结果的中间变量必须保留,不能清栈
2)    一般情况下,从性能上选择迭代,从算法设计上优选递归,而做题以解题为目标,因此应该选择递归法,因为有时候迭代过程很复杂,难以处理
3)    递归的核心是数学归纳法,先假设获得子解,结合已经获得的子解,构造当前递归中的解并返回,最后在递归函数的开头部分,增加最基本问题的解法,基本情况的求解一定是直接计算的,不使用递归
4)    在可以用递归的算法题中,递归十分有用,假设的子解可以非常灵活的自由定义,甚至返回值也可以自由定义,这里有一个技巧,如果需要返回多个值,则可以返回数组,数组是多个数据对象的集合,这个技巧可以大大加强递归的函数的功能
3)    一般情况下,应该首先考虑解决大多数情况下的问题,这是由于大多数情况,逻辑性比较简单;然后,再对特殊情况修补算法,例如输入字符串或数组长度为0的特殊情况,并一一处理,可以增加Ace的几率
4)    优先考虑解出题目,而非过于执着于复杂度,降低程序的可读性
1)    关于一些数据结构内置的方法复杂度,需要看java源代码,一般来说,很容易分析出其中的复杂度,自己就可以作抉择
2)    如果遇到过程很繁琐的题目,如果可以用递归解题,那么多个递归要比单个递归更容易解出题目,但代价是效率的稍许降低,但这是值得的
5)    如果算法题非逻辑性错误地超时,也即单纯因复杂度问题而超时,则可以考虑降低复杂度来修正答案,这种情况多出现在n3到n2程度的转化
1)    降低循环嵌套的次数,改为顺序执行多个循环,可以大幅度降低复杂度,除非特殊的情况,应该尽可能避免三层嵌套
2)    动态规划的核心准则,如果出现过多的重复运算,应该考虑将运算结果存储在数据结构中,然后查表即可;这样数据结构可能是数组,二维数组、Set,list,Map等多种类型的对象,考虑查表操作的性能,应该尽可能选择数组类型或者底层实现是数组的数据结构
1)    自底向上的特点是由小变大,不使用递归,先求小的解并记录,在求大解的时候,不必再次深入,其先求解了所有的最小解,并一一储存,再求更大的解
2)    自顶向下的本质是递归法,特点是由大变小,这里要牢记在每次返回前,记录解作为备忘录以供查询,因此一定是自顶向下考虑的,一定是假设已经获得了下一级的子解,利用该子解和当前的信息构造当前的解,并返回,最后就是处理基本解的情况,基本解一定是直接计算的,不再递归
3)    通常,自顶向下的递归法更容易解题,而自底向上,则是算法的调优
4)    动态规划的数组初始值问题,必须与问题的缓存解无交叉重叠才可以,否则可能会导致效率急剧降低
5)    通常,存放缓存解的数据结构与输入数据结构相同或类似,以数组为例,存储的位置既可以是原先的数组,也可以是与原数组长度相同的新数组,而后者更安全,前者效率高
6)    存放缓存解的数据结构,要根据实际情况为定;此外,该备忘录不仅只有一维和二维,还有三维,而三维一般是难题,不能再高了
7)    在背包问题或买卖股票问题中,将要求解的解与前一个解相互影响,需要对前一个解进行修正。这种情况下,根据数学归纳法,因为我们只关注当前的操作,而这种不断地对已经求过的解的修正,最终一定通向最优解,这是典型的动态规划算法
8)    而如果求得的解不会受未来的情况而作修正,也即每次都是一定得到最优解的一部分,这种情况属于贪心算法
9)    动态规划的备忘录中的解可能是多样的,并不一定和原问题的解直接相关,要解决这样的问题,必须先设计出传统递归算法,然后进而分析临时的重复运算出现在何处,此时修改算法,增加备忘录数据结构,避免此重复运算,进而简化算法
10)    关于数组的动态规划中,遍历整个数组的方法,从递归角度容易解出题目,但复杂度过高,且有时候很难找到备忘录的索引值,进而无法用动态规划解题,有一种例外,当数组内容比较简单例如01状态时,则可以把数组转化为一个整数,该整数可以作为索引key值;而采用分割法,则可以更方便地找到索引值,并记录当前的解,使得使用动态规划更容易
6)    当正面解决问题很难的时候,考虑逆向思维;而有些题目暗藏规律可循,找到规律即可简化算法
7)    合理利用堆栈数据结构,可以解决非二叉树结构的遍历问题
8)    涉及到搜索功能,请善用Hash数据结构,即使意味着构造Hash结构也要做,尤其在原数据结构搜索性能很低的时候,数据结构重建能大大提高性能,但需注意key值必须唯一的条件
9)    在遇到性能和代码简洁之间的作权衡时,如果性能差别不明显,可以牺牲性能降低而简化代码,因为解题上应该优先保证解题准确度
10)    在遇到性能和占用内存空间大小之间的权衡时,如果题目没有要求空间限制,则可以用空间换取性能的提升,例如Map的映射关系,某些情况下,可以把正反映射均都放入map中,虽然空间增加一倍,但正反查找的性能得到了提升;这是因为,插入的开销要小于查找,因此,可以用更多的插入换取更少的查找
11)    分治原则(递归法):
1)    有些问题可以不断地细分为和原来完全相同的子问题
2)    利用递归不断地分解问题,直至触底变为基本问题,基本问题可以直接触底并返回
3)    递归的逐层返回过程,即为解的合并过程,合并后的解一定是原问题的解
4)    动态规划和贪心算法本质上是分治原则的问题的子集
12)    最优解问题:
1)    先用递归法做题
2)    考虑递归法+备忘录构成自顶向下的动态规划
3)    将自顶向下转换为自底向上,提高效率,可选
4)    考虑动态规划是否符合贪心算法的特性,进一步改进算法
2.    注意事项:
1)    牢记java中只有按值传递,没有按引用传递,也即被传递后不会更改原值;这里须留意,传对象引用的时候,赋值是把引用复制一份而已,因此,新变量可以再次被赋予别的对象引用,而原对象保持不变
1)    因此,引用相当于一个指向内存块的指针,可以随意更改指向的目标
2)    被复制的引用,可以直接操作被指向的对象,除非该引用指向另一个对象
3)    引用之间的赋值操作,即两个引用如下图所示,非引用1先指向引用2,引用2指向内存,而是两个引用都指向同一片内存区域,也即某一个引用改变指向,另一个引用的位置不变
4)    引用之间不能互相指向,引用只能指向对象的所占据的内存
2)    变量或对象是否相同的判断
1)    对于基本类型的判断,一律用”==”即可
2)    对于对象是否相同的判断,”==”的意义需要注意
1)    两个引用指向同一个对象,”==”才会返回true;如果两个对象是分别new创建的,则”==”必定返回false,即使内容相同
2)    如果要比较对象的内容是否相同,则可以调用对象的equals方法,大多数java内置的类型对象都具有该方法,例如String类型的判断
3)    对于自定义对象,需要自己重写equals方法,才可以正常进行内容判断;如果没有重写,则需要手动提取内容,进行比对
3)    牢记强制类型转换法则,详见java疯狂讲义,尤其是基本类型的强制类型转换,而char和int值的转换关系需留意
1)    char直接向右转换为int,无需强制类型转换;然而反过来,必须要有强制类型转换运算 (char) 才可以,这是由于char类型的位数小于int值
2)    此外,int是char类型和其余所有类型沟通的唯一桥梁,因此,如果需要将char类型转换为其他基本类型例如short、byte等,必须先转换为int值,再根据位数长度决定是否需要强制类型转换
3)    Integer.valueOf函数与强制类型转换的作用相同,只能强制转换为ascii码,不能转化为对应的整型
4)    如果需要非ASCII码的转换关系,则可以调用Integer.parseInt()函数
4)    静态域static不能访问非static域变量;然而,非static域可以访问static域变量,这里需要注意,static域变量不属于某个对象,即使是private修饰的static变量,访问该static域变量,必须以类名+变量名访问,而不应该用this+变量名(尽管仅仅会发生警告,不会报错),因为static变量在对象形成之前就有了,而this表示当前对象
1)    当形参和域变量名称相同时,对于非静态变量,必须用this+变量来访问;而对于静态static变量,必须用类名+变量来访问
2)    当形参和域变量名称不同时,如果是当前对象的static变量,不需要加类名,直接用变量名即可;同理,对于非static变量,直接使用变量名即可
5)    如果是判断true或false,则应该尽可能简化详细过程,只要返回是否既可,因为过程值不重要,甚至可以是未知状态
6)    字符串内容的比较,务必用equals函数,千万不可以使用”==”比较,否则算法极其容易出现未知的问题,详情见字符串段落
7)    关于&&与&的区别:A&&B,如果A不成立,则B不执行;而A&B,A与B都要执行,因此,判断语句中尽量用&&相当于两步判断,如果A中的引用可能为空,则B中含有null引用也是可以的,因为不会被执行到;|与||的区别可以类比
8)    原地算法,in place,可以允许O(1)的额外空间,但O[n]等不行
9)    如果对象引用为null,则该对象的引用不能被二次传递,这里称为传递性中断;也即只有非null指针的对象引用才可以被按值传递
例如,TreeNode x = null
1)    treeNode y = x; 
2)    y = new treeNode(2);
3)    x仍然为null,这意味着,空指针null不能通过引用传递给函数内部
4)    null不是对象引用,不具有传递性
5)    为了避免错误,严禁传入空null指针给函数,原因有两种:
1)    对象引用为null时,在不知情的情况下,引用该对象的元素或方法,会有空指针报错
2)    如果一个对象为null,则该对象引用不能再次二次按值传递,也即传入函数中的null对象指针,会彻底丢失原有对象的结构,不能对之前的对象进行修改;尤其是该对象是所要返回的结果时,很容易在在不知情的情况下,造成空操作
3)    总之,如果不能避免传入引用为null,则要用判断语句单独处理
a)    保证不对已经是null的对象进行二次引用,因为null指针由于传递性中断,已经丢失了所有对象的信息,不能再次被引用;
b)    而对于已经是null的对象指针,如果需要递归,则改为直接传入null,而不能传入已经是null的引用对象的内部属性
6)    如果发现传入函数的对象可能为null,立即修改函数,删除该可能导致空指针的变量,代之以类的全局域变量,在函数内直接操作域变量,不经过形参这一步最为稳妥
这是由于第一行相当于 y = null,如果具有传递性,则必然会影响整个程序的初值,例如所有的null值都为某一个对象的引用,这会引发问题
3.    递归:
1)    递归的好处在于保存栈的信息,不会清栈
2)    善于利用之前的栈的信息,可以极大地挖掘递归的性能
a)    安全性,即带入函数后,不必担心该值发生改变
b)    整型变量由于其安全性,随时记录树的深度
c)    字符串变量的不可变性,因此具有安全性,以及随时保存旧字符串
d)    普通对象由于不具有安全性,因此需要留意对象的时刻变化;如果想要使对象具有安全性,则必须重新创建对象的副本
e)    如果要祛除字符串和整型变量的安全性,即使得整型变量和字符串变量在递归中永久更改,则可以考虑类的全局私有变量的方式
4.    循环:
1)    For循环最大的优势在于,循环变量的规律递增性,也即每一次循环体执行期间,循环变量一般不会改变(除非手动二次操作),直到循环体结束
2)    While循环最大的优势在于循环变量的灵活性控制,也即循环变量的递增需要手动控制,这可以是在循环体的任意位置,而非for循环中的末尾处才改变
3)    如果循环变量的变化是有较为明显的规律的,应该尽量用for语句,for语句更容易理解,而放置在while中的循环变量不直观
4)    For循环中循环变量的更新,是在循环体执行完成后,才执行自加的,因此如果需要在循环体中操作循环变量,则应该考虑其循环体执行后,本身的自加性
5)    判定一个过程从某一刻开始,无限循环下去的方法:一是HashSet;二是快慢两个过程,如果在相同点相碰撞,则包含无限循环,最好速度倍数为2
6)    嵌套循环的降阶处理方法:
1)    如果循环的循环次数为一个不大的常数,如10、100等,则该循环的复杂度为常数,实际上,这种情况属于徒有其表,而不属于嵌套循环,故不必处理
2)    传统的嵌套循环之所以复杂度高,是因为每一次的外循环内部必定执行一次复杂度为n的内循环,这种情况可以考虑给内部循环是否执行加判断语句,这就可以降低复杂度,因为避免了冗余的运算,这种情况下,最差情况为平方次,最好的情况为线性时间,但基本上接近于线性时间
7)    对于可能会多次循环相同操作的嵌套循环或者单循环
1)    这个时候用递归不恰当,因为递归的后续操作会造成不可预料的影响
2)    而做成迭代形式,加一个flag标记是否继续重复循环也很复杂
3)    这时候,可以考虑采用重置for循环的循环变量的方法最为方便和快捷,如果是嵌套循环,则可以重置外循环的循环变量,而后break终止内循环,进而达到重复整个嵌套循环的效果
5.    二叉树与二叉搜索树:
1)    两者都不能保证是平衡的,也即不一定是完全二叉树,后者的高度为lgn
2)    所有的算法题,如果不强调,都默认二叉搜索树的元素key值各不相同,也即每个元素值都不相同,否则会极大地复杂化算法;而普通二叉树key值可以相同
3)    灵活运用三种方式的遍历方法解题,在遍历中,可以在通用遍历代码基础上,添加自定义的判断条件,决定是否进入下一层,会减少很多麻烦
4)    只要是普通二叉树,必须考虑重复元素的情况,比如Set集合不能存储相同的元素,进而导致出错;而二叉搜索树key值一定互不相同
5)    二叉树没有明确的递增规则,也可以包含相同的元素;而二叉搜索树,可以保证左子树的所有节点与右子树所有节点和父节点之间构成递增关系
6)    活用整型变量的按值传递,使用两层嵌套,在递归中记录当前的深度值,这是重要的纽带;使用此方法,需注意进入空子树使得i>h的异常(该异常会造成很多不可预料的错误,因此需要尽可能避免),因此加入判断条件—左右子树均为null时,不执行递归,直接返回;可以避免i值大于树的最大深度h
7)    与字符串等变量一样,需考虑空树的特殊情况,即根节点为null
8)    在树的遍历中善用栈结构,来存储通过的路径
9)    二叉树的还原,也即由遍历数组还原二叉树问题:
1)    得到树的左边界或者右边界:已知中序遍历数组,首元素即左边界,末元素为右边界
2)    得到根节点:如果是前序遍历,则为首元素;如果是后序遍历,则为末元素
3)    基本原则是利用递归,递归内部调用递归设置左右子节点,返回当前节点,出发点是根节点开始,在两个数组中,计算下标差值,寻找左节点或者右节点
4)    中序遍历作为被搜索数组,由于中序遍历的特殊性,假设某子树的范围是[a,b],子树中的根节点为位置为c,则[a,c]为左树,[c,b]为右树,也即可以得到子树的总节点数,递归中要实时调整当前子树的起始和终止范围
5)    由后序或前序遍历,可以得到当前树待求的左子树根节点或右子树根节点,方法是root在后序或前序遍历中的index与子树总节点数的差值或求和
6)    递归的终止条件,前序或后序遍历数组越界或中序遍历的范围左边界>右边界的时候,返回null即可
7)    实际做题过程中可以画一个小子树,方便换算下标,因为这里极易出错
6.    集合:
1)    集合的声明方法采用java 8的方式比较方便,尤其是集合的嵌套方式声明
2)    集合只能存放对象,不能存放基本类型,特别的,数组属于对象,因此可以储存
3)    关于toArray(T[])的用法,与toArray()不带参数的不同
1)    toArray(T[])必须先手动创建数组对象,并设置大小,然后直接填参数不必返回;大小的设定非常简单,只需要新建数组时,长度设置为集合的大小即可
2)    toArray()直接返回数组对象,因此只提供引用即可;然而,其会丢失原有的内置类型,返回类型为Object[]数组,因此需要强制类型转换
3)    该方法的局限性是不能直接返回基本类型的数组,例如int[];如果需要返回基本类型,则需要手动遍历原集合并手动为数组赋值,比起使用toArray方法更为快捷
4)    综上所述,使用前者的范围更广泛和普适
4)    普通顺序表
1)    ArrayList底层以数组实现
2)    LinkedList底层以链表结构实现
3)    效率比较,同队列和栈的两种实现
4)    同队列和栈,为了方便解题和记忆,可以一律使用ArrayList,因为性能差距不明显,因为遍历和插入往往解题中会同时用到,因此可以简单处理
5)    队列和栈
1)    对于二叉树结构以外的各种类型遍历,要善用堆栈系统,缓存数据
2)    LinkedList插入和删除等操作效率高,因此当频繁地插入删除操作时,应当使用该实现
3)    ArrayDeque在遍历上效率最高,因为底层的数组实现是直接随机访问,因此当出现频繁遍历的时候,需要使用该实现
4)    为了方便记忆,排除性能考量,可以一律选择ArrayDeque,因为两者性能差距不十分明显,因此为了方便解题可以简单处理
5)    队列的两个常见方法:peekFirst或peekLast不删除元素地访问;pollFirst和pollLast删除元素地访问;同理,addFirst和addLast是在头部或者底部加入元素,poll和peek默认指向首位,而add例外,指向末尾处添加
6)    (堆)栈的两个常见方法:push和pop更为普遍和易于使用
6)    遍历
1)    对于底层是数组实现的集合,如以Array前缀开头的集合,其随机访问效率最高,因此,用循环中的get方法遍历,效率最高;例如ArrayList,ArrayDeque
2)    对于底层实现是链表的集合,应该使用for each方法进行遍历,因为迭代器访问链表效率较高,但仍然比数组效率低;例如LinkedList
3)    对于集合的遍历过程中,不允许修改被遍历的集合,否则会报错,解决思路是新建一个集合,缓存被删除或者添加的元素,遍历结束后,使用removeAll或者addAll函数一次性操作完成;map只有putAll,没有removeAll函数,只能一个个删除
4)    对于map函数
1)    可以是keySet函数或者values函数获取相应的集合,此时可以利用foreach语句直接取出集合内的元素,适合于只需要遍历value或者key的情况,效率较高
2)    如果是同时遍历key和value,而是采用KeySet方法来遍历并实时查询,来获取所有的key和value
1)    实际上,上述方法并不是效率最高的,却是最方便的,尤其是做题过程中,因为Entry类型在做题中不方便使用
2)    工程中,若要求效率最高,则需先使用entrySet函数,得到Entry泛型类型,同理可以利用foreach函数直接取出相应的值,再根据getValue和getKey函数,取出相应的值
7)    搜索:
1)    关于hash值和equals函数的关系
1)    如果equals为true,则hash值一定相同,也即hash值与对象存储的地址无关,新建两个相同equals为true的对象,hash值必定相同;如果hash值不相同,则对象的equals一定为false
2)    但如果equals为false,则hashcode仍然可能相同
3)    总之,由于hash值是集合内部的中间值index,一般不会用到
4)    搜索功能中,最终仍然是通过equals函数判断对象相等的;
5)    对于equals,顺序相关的集合,equals是顺序有关的;而顺序无关的set类,equals顺序无关,也即添加顺序对于set类无关紧要
2)    当出现冗长的判断语句时,且为连续的”==”判断式(已经接近于查找判断的形式时),则应该考虑HashSet的contains功能,予以替代,一是,代码更加简单,二是后者的平均时间复杂度是O(1),优于连续判断复杂度为O(n),这是由于连续判断类似于链表查找,复杂度高
3)    对于Hash类,搜索功能请善用contains函数,不要尝试add判断boolean值的方法,因为当添加来源中包含重复元素的时候,会误以为集合中含有了之前添加的新元素,contains比较安全,不会对集合做改动
4)    链表实现和数组实现的搜索效率,数组实现优于链表;
5)    而顺序表结构自带搜索的函数可以使用
6)    涉及到搜索功能,请善用Hash数据结构,即使意味着构造Hash结构也要做,尤其在原数据结构搜索性能很低的时候,数据结构重建能大大提高性能,但需注意key值必须唯一的条件
7)    Hash类的数据结构搜索和插入效率最高,但Hash不允许重复值
8)    HashSet为普通的Hash集合,搜索效率最高
9)    TreeSet为以红黑树排序好的Set集合,只有在添加完元素后,必须有排序整个集合的步骤时,才考虑用TreeSet,因为只有这种情况下,TreeSet的性能有优势,如果没有必须要排序这一步骤,统一用HashSet
10)    LinkedHashSet内部以链表形式维护插入顺序,但由于链表的加入,使用迭代器遍历该集合效率较高,但搜索和插入等性能低于HashSet;
1)    总体来说,使用情况较少;除非要维护插入顺序,否则一律使用HashSet较为稳妥
2)    虽然该集合为链表结构加入了良好的搜索性能,在搜索上优于传统链表,但其付出了不含重复元素的代价,使用时需要极其留意
8)    常用操作
1)    Collections工具集可以对集合进行重排序,例如正序排序sort,倒序reverse,或以某一中心点旋转rotate,旋转函数比较复杂,不常用
2)    List的添加是add函数,插入也是add函数,需同时指定次序和元素;替换操作,list是set函数,需提供index和元素;删除都是remove函数
3)    map的插入是put函数,替换是put或者replace,删除是remove,map需要提供key值,查找containsKey和containsValue,这点不同于HashSet的contains函数;在某些情况下, getOrDefault方法也很实用,同时搜索并返回,这在自加中非常重要,而不必搜索两次
4)    TreeSet比传统HashSet函数多出的操作,first、last、higher(严格大于)、lower(严格小于)、floor(小于等于)、ceiling(大于等于)、pollFirst、pollLast等操作,最后两个操作会类似于出栈,会删除元素;对于TreeMap相应的方法名+Key即可
1)    由于TreeSet使用极少,因此一般情况下不考虑,而且其基本操作复杂度为lgn,n此操作就是nlgn,如果需要完整的排序好的序列,可以考虑
2)    而用HashSet时,n此操作的复杂度是n,加上排序好,则是n+nlgn,因此使用TreeSet更为方便
3)    然而,如果仅仅是取出最大最小值,使用HashSet的复杂度是n+n也即2n即可,要比简单TreeSet的nlgn的复杂度更低,因此,大多数情况下应该使用HashSet;如果添加完成后,必须有排序整个集合的步骤,则使用TreeSet复杂度更低
4)    实际上,仅仅是取出最大最小值,不需要用到HashSet遍历,直接在子过程中比较就可以,可以大大简化复杂度,上述仅仅为举例说明
7.    整型:
1)    考虑每个变量的取值范围,如果可能溢出,例如超过int值的最大值,则应该考虑将int修改为long
a)    Integer的最大值为2147483647即(2^31 - 1);最小值为-2147483648即(2^31);十进制情况下,最值一共10位;而二进制情况下,则为31位+符号位
b)    整型值的负数最小值,取绝对值不可以用同类型,否则会溢出;例如就绝对值来说,负数最小值的绝对值比正数的最大值大1,因此,对于最小值需要谨慎处理绝对值的问题,避免溢出
2)    注意整型的正负情况,需留意题目中的讲解,未说明即可正可负
3)    取出每一位的方法:取商取余法最为方便,转为字符串效率低,而且字符串很难直接转换为整型,因为char与int区别很大
注意:整型不能和char直接用构造函数转换,强制转换是根据数值形式地ascii码来转换的,如果想直接由数字转换为相应的字符类型数字,则必须先根据差值,计算出ascii码,然后强制类型转换,不能直接转换;有一个例外,Integer I = new Integer(“123”),该方法可以把字符串直接转换为数字整型,仅此特例,且不能逆向转换,该方法如果包含无法解析的非数字字符,会抛出异常
4)    无符号数的表达:
1)    long i = 3147483648L,如果要得到int值的无符号类型,则可以int u = (int)i,此时,使用system输出函数,也不能正常输出该无符号数,因为最高位被视为负数的符号位,0为正,非0为负
2)    如果要从无符号u中取出该无符号数,必须将其恢复为long类型
1)    long I = u & 0xFFFFFFFFL,此时原本的3147483648L,注意,如果不加L则会被视为int类型
2)    long l = u,右边由于是int值,因此最高位被视为符号位,因此转换不正确,其结果是-147483648L
3)    善用各种后缀可以灵活的表示数据
5)    十进制转n进制
1)    取商取余法:方法很繁琐,该方法遇到处理无符号数前,需要先把有符号数转换为无符号数,也即int值转为long类型,具体方法见4)
2)    移位法:由于整数在计算机中以二进制形式储存,因此可以直接对数进行二进制移位运算,并以二进制运算的方式取出每一位二进制
1)    无符号法:最高位也算作数值位,>>>表示连同符号位在内右移1个bit
2)    右移位最终结果一定是0
3)    取出最低位的n个bit,用原数&N即可,即可一个个地逐步取出低n位数;需注意&运算一定是将符号位视作数值为的,不必担心结果的正负号问题
4)    对于二进制,N是1;对于八进制,N是7(111);对于十六进制,N是15(1111),原数&N得到的数字,自动转换为十进制int,之后可以利用数组查表法,轻松提取出对应进制的字符
5)    该方法可以处理正数和负数,唯独0不可以,需要单独处理;其中负数的结果是补码形式,计算机内部不会存在负数的原码形式;因此,如-16的原码二进制实际上是另一个负数的补码,与-16没有任何关系
6)    如果非要求负数原码形式的二进制形式,则只能用取商取余再加符号位的形式,手动拼凑来获得;因为就计算机来说,两者毫无关系,只能用实际生活中的算法形式来做题,不能通过计算机内部的数据来转换
3)    综上所述,第一种方法太过于传统和古板,因此抛弃
6)    整型基本变量的表示
1)    前缀:用于标记进制数,如0b、0、0x分别表示二进制、八进制、十六进制,默认为十进制
2)    后缀:用于标记整型类型,默认为整型变量,在某些情况下可能需要强制转换为特定类型,例如L表示long类型;该方法常用在int值的无符号转换实现中,因为该方法可以将int值中的符号位一起视作数值位转换为long类型
3)    所有的整型或者浮点数,默认视为有符号int或者double
1)    long i = -2147483649,此时右侧仍然被视作int类型,因此会出现溢出编译错误
2)    而long i = -2147483649L,该语句会直接将右侧视作long类型,因此不会出现
3)    float a = 1.3,编译错误,因为小数默认被识别为double,因此float a = 1.3f才是正解
4)    10000000与00000000不相同,左边是负数的最大值,右边是非负数的最小值0,这也是为什么负数比正数绝对值大1的原因
7)    基本运算:
1)    加减法,加法可能造成数据溢出,而减法相对安全,尤其是和已知的情况下,减法会方便不少
2)    取余或取模:%,例如,累加并取模,满足结合律
1)    sum = (sum + x) % mod即可
2)    为了避免中途数据溢出,最好在每一次相加运算后,立即取模
3)    按位运算符:对整数的操作,例如异或^,|,&,~等
4)    比较运算符:用于判断语句中比较数字大小,由非boolean得到boolean值,例如<、>等
5)    逻辑运算符:用于操作两个boolean值的运算,例如!非,&&与,||或
6)    整型二进制加法:
1)    先与运算,得到进位的二进制信息,并缓存计算结果
2)    先按位异或,任何进制的数,都支持二进制形式的异或运算,得到不考虑进位的加法结果,赋给被加数
3)    将第1)步中的进位信息<<1后,赋给加数
4)    重复上述两者运算,直到加数为0终止运算,返回被加数
5)    该算法即使不用+-号也可以迅速完成加法运算,而且效率高于后者
6)    该方法可以适用于加法和减法,这是因为计算机内部用补码形式运算,统一了加减法运算,都为补码的加法,而计算机呈现的是补码的补码即原码的结果,实际上我们对数据的任何操作并不是真正的原码运算,而是转化为补码,再计算机内运算,然后逆向为我们常见的二进制结果,其实运算结果没有任何不同
8)    Production意思是乘积
9)    Prime number的意思是素数,也即质数,判断依据:
1)    特别的,所有的质数都大于1,最小的质数为2
2)    对于大于2的数,判断2?根号n之间的所有数,包含边界,如果可以整除,即不是质数;
3)    关于合数,1不是质数也不是合数,因为合数必须有大于2个的约数;而质数有且仅有2个约数,分别是1和它本身,而1仅有一个约数
8.    数组或矩阵
1)    注意空数组情况,注意数组长度的个别性,例如空数组情况;而有些题目也会及时指出数组长度为正值,需要注意
2)    数组的动态初始化,如果不指定元素值,则默认为0或者false等等;而对于对象元素,则默认为null
3)    数组的遍历方式,不同于集合,在数组遍历中,for each语句和传统for循环语句差别并不明显,因此可以根据是否需要循环变量而选择即可
4)    二分搜索算法:
1.    由于Arrays自带的二进制搜索函数binarySearch功能有限,只能提供大小比较方面搜索,而实际上搜索定位的算法多种多样
2.    关于手动实现二进制算法的复杂度分析,一般用决策树模型分析二进制搜索的复杂度,而其他的算法分析方法会变得很复杂
3.    例如,在已经排序后(一般是按照大小排序,具体因题目而异),1->n包含边界内寻找某一个m,利用中值方法判断,但会出现找不到n边界点的情况,这里有两种解决方法:
1)    一种是让搜索上边界为n+1,这种方法有缺陷,加法可能会造成溢出
2)    另一种是算法最前面加上n值判断,以排除个例,一般情况下,采用这种方法
3)    为了实现搜索不到时返回-1的功能,需要先判断上下界相等时或者差1时,分别判断是否等于target,否则返回-1即可
4)    需要注意的是,二分法只需要考虑首次执行二分时的右边界情况,至于递归调用的情况,实际上代码的内部逻辑已经的得到了避免,或者说已经得到了相应的处理判断
4.    此外,对于取中值经常可能出现int值溢出情况,这里同样有两种方法
1)    一种差值/2+下界/2,虽然繁琐,但该方法是唯一可用且安全的,推荐
2)    也可以根据题意,扩大存储变量的类型,例如long等;但此方法使用有局限性,例如在强制要求int的场合,不可用,例如某些函数强制接收int值,但该值为取平均值得到,当然也可以,中间变量强制转换,但总体上在这种情况下,第一种方法更简便
3)    上界/2 + 下界/2,会造成数据误差,因为损失了小数点,因此该方法废弃
5)    二维数组
1.    牢记二维数组(矩阵)的行数和列数,由于x, y容易与坐标系的横纵坐标混淆,因此不推荐用x, y分别标记行数和列数,而又由于其与坐标系恰好相反,容易出错;同理,也不推荐用i, j标记,而一般用row或col来标记,或者直接简写为r和c分别表示行数和列数,可以避免一个极为常见的错误
2.    子方阵求和问题:用track二维数组记录,该二维数组记录当前位置和(0, 0)位置组成的子方阵的和,这很好实现;
1)    扫描方向,在每行开始时,令sum = 0,sum仅代表当前行的和,加上之前求得的结果,即可求出子方阵的和
2)    需注意首行的话,需要单独处理,否则会越界
3)    根据左上角的顶点坐标,对于任意子方阵的和,只要减去左边和上边的方阵和,并加上左上角的方阵即可,因为左上角的方阵被减了两次;其余的情况则要更加简单,判断语句处理即可
4)    注意结果的坐标处理需要-1处理,因为track包含本坐标,需要错开位置
6)    Arrays工具集
1.    关于数组的复制,使用Arrays工具集,只能对一维数组进行复制,即浅复制
1)    这是由于,二维数组相当于嵌套对象,因此单个元素被当做对象引用复制给新二维数组,也即改变会有连锁反应
2)    java中没有二维数组,只有数组的嵌套;若要深复制,则需要手动操作
3)    总之,复制功能并不常用,一般采用手动实现可以避免很多问题,因为过程比较清晰
2.    关于binarySearch函数,必须要求数组事先被自然排序;需注意,这里如果搜索不到,结果仍然具有参考价值;其返回值为:- (插入位置index) – 1;
1)    因此,返回值+1然后取绝对值,即可得到插入位置,这在某些算法题中非常重要;当然,结果为负值,代表没有搜索到该值,但二进制搜索可以提供额外的信息
2)    总而言之,二进制搜索的返回结果,经过小于0判断并处理后,可以永远得出其插入的index位置,这非常重要;如果找到该值,仍然是该值的插入位置
3.    关于数组的输出,一般情况下,数组不能直接输出,char[]数组是为数不多的可以直接输出的数组类型;Arrays.toString()方法,可以处理数组输出问题,故调试中,都十分有用
注意:集合不需要处理,可以直接输出,这也是因为集合内部不含基本变量,都是基本变量的封装类型
4.    Arrays不如集合Collections工具强大,后者还有倒序,旋转等功能
7)    牢记java中的数组是对象,标识即为引用
8)    对于数组水平翻转,循环变量的范围直接从0 <= i <= (a.length-1) / 2,可以直接得出,这里左右都有“=”便于记忆;而对于对调的标号差,可以从首尾标号相加和不变性考虑,例如0,length-1对调,和永远为length-1;因此,可以直接推导出,length-1-i和i
9)    对于数组中的中点取值,直接用标号相加取平均值即可,以(5)为例,首尾和为a.length-1,因此直接对2取商即可
10)    关于求数组中所有元素的组合问题:
1.    数组先排序,保持解的规律性,可选
2.    嵌套集合中先放null,遍历该集合,保留原有元素,为每一个元素增加一个新元素构成新的组合,并放入嵌套集合中
3.    遍历完数组后,求解完成
4.    需注意嵌套集合建立缓存,然后addAll一次性加入,如果没有缓存,会在遍历中就更改了被遍历集合而导致混乱
11)    关于求数组中所有元素的组合的求和问题:
1.    常用方法为递归法
2.    递归中包含循环,循环中迭代本递归
3.    如果需要排除一些选择,则需要结合动态规划的track数组作标记
4.    递归参数中,需要指定循环的起始位置或终止位置,在首次组合时需要利用动态规划(调整起始值)主动避开已有的组合,而下一次寻找需要将起始位置设置为0
5.    由于本例中用到了多次循环,而过于依赖track判断来跳过循环,并非最佳方案,因为判断本身是需要运算的。实际上,本题中如果不在递归中适时地重置或调整循环起始值,则动态规划实际上是失效的,因为重复的组合被选出
12)    关于连续子数组求和逼近问题
1.    第一种是暴力解法,两层嵌套,直接强行求和,如果长度受限,且子数组长度没有硬性限制,例如大于2
2.    第二种是map统计法,边统计边查询,由于当前和-之前和=中间和,因此如果当期和-中间和(所求目标)==之前和的时候,即找到了子数组,该查询结果为子数组的个数;存储的和一定是从标号0开始到当前标号的sum值;map应该事先放入(0,1)以便统计为本身的子数组;该方法不适用于数组长度受限制的情况,因其算法会变得非常复杂
13)    关于数组的三数组合之和无限逼近问题:
1.    一种基本思路是三层嵌套,可以考虑到所有的可能相加和,并从中选择最接近或者等于该目标,同时也可以轻松地得到这三个数,但这种方法有两个缺点:一是,复杂度O(n3)太高,一般情况下,会超时错误;二是,无法从众多可能中,避免三个数的组合的重复出现
2.    另一种思路是,降低复杂度为O(n2),而且可以避免重复组合出现;首先对数组进行顺序排列,然后从0 -> length – 3(包含边界,以腾出left和right的空间)作循环,当i > 0 && n[i] == n[i - 1]时,直接跳过循环;在循环中,left = I + 1; right = length – 1,求和,并判断:
1)    若大于目标值,则right--;若小于目标值,则left++;
2)    若等于目标值,则这里需要处理重复的组合产生;若n[left + 1] == n[left],则left++;若n[right - 1] == n[right]则,right--,分别做while循环;最终,需要再次left++,right—跳过最后一个重复值
3.    总之,在不需要计算所有的可能性的前提下,第二种方案由于事先排序,可以自行跳过诸多无用的相加和(也即如果和已经远离目标,则无需对更远的和进行任何处理,直接跳过),从而将复杂度降低了一个数量级
4.    第一种方案在大多数算法题中,极其少地使用,因为通常三阶复杂度一般都会超时,事实上,大多数的题目都不会超过平方级的复杂度,甚至平方级的复杂度超时也不罕见
14)    数组状态的保存:
1.    如果是只有01,则可以用先移位,后对个位取或运算即可,得到一个独一无二的整数,该整数代表一个数组状态,可用于动态规划的key值
2.    如果含有01以外的数字,则根本思想是转换为一个整数,实在不行就是字符串,再者重新复制一个数组也可以,作为数组状态的记录key值以供查询
3.    *数组元素的统计法:对于数组内容的大小被限定的前提下,可以设定一个新数组,数组下标为限定范围,内容为该数字的个数,该方法类似于字符串的字符统计法,此外,该方法把数组内容归类后,还按照顺序排列,仅需要n的复杂度,当然,该方法对数组有限定要求
15)    背包问题或买卖股票问题
1.    动态规划问题,每次可以作出两种选择:一种是放入,另一种不放入
2.    如果放入,则需要对上一次结果进行重新修正
3.    如果不放人,则结果与上次相同
4.    二者取最大值作为新的结果
9.    Math工具集:
1)    向下取整floor函数(地板值也即抛去多余的小数值),可以用于判断是否为整数,向下取整与原数相等(或者强制类型转换为整数也可以判断是否为整数),这里1、1.0都是整数
1)    需注意,floor函数返回类型为double,也即1.2返回1.0,而强制类型转换则不存在这个问题,结果始终为整数
2)    1.0与1、1.00是相等的
2)    向上取整ceil函数,返回类型为double
3)    四舍五入用round函数,double返回floor,floor返回int
4)    Sqrt开根号,返回类型为double
5)    对于比较大小时,min或max函数,初值正好反过来,即min函数初值为Integer.MAX_VALUE;而max函数初值为Integer.MIN_VALUE;
10.    字符串:
1)    注意空字符串情况
2)    遍历字符串有两种方法:
a)    使用charAt()方法,而长度使用length()方法即可
*关于获取长度的方法,集合为size(),数组为length
b)    toCharArray()方法,先生成数组,注意,此方法内部实现会分配新的内存块,非直接返回,因此效率不如前一种方法,但代码简单,尤其是在遍历中
c)    总之,应该优选第二种;尽管性能上应该优选第一种;但第二种代码简单不易出错的话,事实上,在做题中,优先不出错尽快解题最重要
3)    正则表达式相关:注意转义字符,例如”\”本身,直接放在双引号中,是不能打印出来的,因为转义字符单独使用会自动与后面的字符匹配,从而可能招致错误;然而,两个”\\”则表示斜杠本身,可以正常打印;由此可知,在正则表达式中,如果遇到转义字符,则需要通过两层防护,第一层,字符串本身,第二层,正则表达式;例如:
a)    “\”本身没有任何意义,无法打印出来,必须与后续字符组合才有意义,如 “\n”
b)    “\\”表示字符串中的’’\”本身,可以正常打印;然而在正则表达式中,则表示单个”\”本身,其毫无意义,必须有后续字符,例如”\\.”在正则中表示”\.”,才有意义
c)    “\\\\”在正则表达式中,表示”\\”,也即正则中的”\”本身,如果需要匹配斜杠本身,则需要”\\\\”才可以
4)    字符串相等的判断:==必须是同一个对象的引用才返回true,而两个不同的内存块返回false;而equals只要内容相同即可,可以是两个对象
因此,比较内容是否相同的,多数情况下,用equals
5)    关于顺序一致的子字符串的提取
a)    请使用substring函数,注意函数名全是小写,左边界包含,右边界不包含,类似于Random函数的nextInt用法
b)    顺序字符串的搜索问题:请使用indexOf或者lastIndexOf,返回首字符的位置
6)    关于乱序子字符串的搜索,该方法可以直接当做做题模板来使用,详情见LeetCode 438;除了该用途,该方法不宜执着使用,如果使用该方法极大地加大了复杂度,也应该舍弃,但就乱序字符串搜索,则只能使用该方法
a)    此时substring函数不再可用
b)    尤其注意,关键字数组应该如是声明:int[] ss = new int[256];并遍历关键字key字符串得到char作为下标,直接存入该数组即可,重复元素自加1即可,由于char类型对应1个字节即8bit的整型,因此char存储时,会自动转换为整型下标,且不会越界
c)    然后遍历源字符串,在对应关键字数组的相应下标+1或者-1;有两个指针,left和right,初始值都为0;right++直到达到关键字长度,再移动left++;
d)    每次循环中,用自定义valid函数,遍历关键字数组,是否全为0,如果是,则返回true,表示找到字符串,如果发现非0,直接中断验证并返回false
e)    而利用嵌套循环复杂度急剧上升,因此子字符串的搜索不应该用此方法
f)    如果是字符串为非乱序,可以直接用substring函数
g)    作为字符统计,对于只有字母的情况,用该数组法统计极佳;如果被统计的字符拓展至任何字符,不仅含有字母,那么此时数组法不再适用;则此时可以考虑HashMap及其getOrDefault方法,可以得到接近于数组法的极佳性能
h)    数组统计法可以根据数组的值,也即统计数sort排序;而map则很难根据values的值进行排序
7)    关于字符串的对调,由于String类型的内容不可变性,需要将其转换为char[]数组类型,改变内容,之后通过构造函数转化为String,使用new String(char[] ss) 函数
8)    由于String具有不可变性,如果需要String可以修改并累加拼接,并使用类似于对象引用的传参方法,则使用StringBuilder类型,可以视为真正的String的对象类型
1.    如果使用String实现StringBuilder类型的特性,则需要重复创建大量String变量,这大大增加了算法的复杂度,不推荐
2.    总而言之,出于安全性,需要利用String不可变性的需要使用String类型;反之,频繁更改字符串内容的,直接使用StringBuilder类型可以大大地提高性能
3.    String引用的拷贝无法改变字符串的内容,因为String类型不可变,只能重新创建新的内存块,回收丢弃旧的字符串,造成效率大大地降低
9)    回文校验:
a)    判断字符串是否为回文:,先转换为数组,然后while函数i++,j--法,i,j初始位置为首尾下标,判断是否相等,直到I > j
b)    判断字符串内总共含有多少子回文,用奇偶校验法:区别于判断单个字符串的首尾校验法,这里采用由内而外法,即令i = j = index或i = index且j = index + 1,其中index从0 –> length – 2;
10)    字符的大小写ascii处理:由于实际做题中,可能无法知道具体的ascii码,如需要转换时,可以利用差值,如A-a,需要注意的是,A的ascii码小于a,此外,a和Z的ascii码并不连续,需要尤其注意
11)    *字符串不可变性的理解:
a)    String s = “abc;” //相当于String s = new String();
b)    如果重新赋值,即s = “cde”,则非改变了原内存块,而是新建了内存块,原先的内存块丢失被回收,这是因为”cde”相当于new String();
c)    这是int i = 0; i= 2; 与String s = “abc”; s = “cde”的细微不同之处
d)    为了便于记忆,可以把int,Integer,String归为同一类,即传参数的时候,是内容的拷贝,而非引用的拷贝(尽管细节上很复杂,实际上仍然是引用)
11.    对象参数的传递:
1)    一般情况下,对象都是按照引用传递的,也即多个引用指向同一个内存区域
2)    然而对于String和Integer等,内部的所指内存区域,不能被修改,隐含内部的装箱对象的final声明;对于这种类型,应考虑声明一个数组,可以解决不能修改的问题;更为简化的记忆:将String、Integer等类型看做对象内容的拷贝,而非对象的引用的拷贝,因此修改传入的参数,无法修改原对象的内容
3)    Java中的引用类似于C++中的指针的作用
12.    随机数生成:
1)    新建new Random()对象,nextInt(int bound),返回0->bound-1之间的所有数值,不包含上边界
2)    对于任意上下界之间的随机值,经过简单的叠加最小值换算即可
13.    图算法:
1)    图的表示,用一个HashMap,这是邻接链表的形式;当然,也可以用数组,但并不常用;邻接链表可以处理绝大多数的问题
2)    图算法两个基本算法:DFS和BFS,由于DFS涉及到递归,因此DFS的题目比BFS更为普遍和有难度;需要注意的是,BFS不涉及递归就可以完成
3)    BFS用于发现最短路径,而DFS用于快速发现底部节点
14.    BFS:广度搜索最简单,拿最简单的遍历来说,甚至不需要递归,整个过程需要三个数据对象,深度搜索每一次都会先扫描当前节点,再立即扫描其子节点,类似于树里的先序遍历
1)    不需要递归
2)    HashMap存储源数据,是全局变量
3)    ArrayDeque存储等待扫描的节点,该队列随时更新,每次扫描一个节点时,会先poll掉当前的节点,然后得到邻接的点,加入队列,外部大循环判断条件是该队列为空时终止
4)    HashSet代表扫描过的节点,为了减少运算,在向队列中增加数据时,需要先确认该节点没有被扫描过,同时,set也会随着扫描的进行,随时增添新的元素;如果需要存储被扫描的节点距离根节点的距离,那么可以换作map即可,value就是距离,至于距离的计算,借助上一个节点的value + 1即可
5)    外部大循环的开始处,会立即弹出当前一个节点最为当前节点
6)    遍历函数只有一个参数,start代表第一个被扫描的节点,在一开始就需要加入队列中
7)    使用队列的原因是保证扫描的顺序不乱掉,并不是随意的
15.    DFS:深度优先搜索,参数有两个个即可
1)    需要使用递归
2)    存储源数据的map,是全局变量
3)    一个存储节点状态的HashSet:存储已经扫描过的节点;如果需要额外的信息储存,可以换作map,value是要存储的信息即可 
4)    另一个参数是开始扫描的节点,由于是DFS,而且是递归,因此start参数是随时更新的

你可能感兴趣的:(算法导论学习)