总结了一下网路资源解决了一道leetcode上的题目。
Java 中实际上提供了 java.util.Stack 来实现栈结构,但官方目前已不推荐使用,而是使用java.util.Deque 双端队列来实现队列与栈的各种需求.如下图所示 java.util.Deque 的实现子类有 java.util.LinkedList 和 java.util.ArrayDeque .顾名思义前者是基于链表,后者基于数据实现的双端队列.
要讲栈和队列,首先要讲Deque接口。Deque的含义是“double ended queue”,即双端队列,它既可以当作栈使用,也可以当作队列使用。下表列出了Deque与Queue相对应的接口:
![2838901cc3df4f39910136642803e0bc.png](https://i-blog.csdnimg.cn/blog_migrate/a73a3cdbfd64ab76c5c31ee0312f77ad.jpeg)
下表列出了Deque与Stack对应的接口:
![38dd3ee64c1685e02ab009a733bd996b.png](https://i-blog.csdnimg.cn/blog_migrate/5fd037417d02db5d2703bd718fe7399b.jpeg)
上面两个表共定义了Deque的12个接口。添加,删除,取值都有两套接口,它们功能相同,区别是对失败情况的处理不同。一套接口遇到失败就会抛出异常,另一套遇到失败会返回特殊值(false或null)。除非某种实现对容量有限制,大多数情况下,添加操作是不会失败的。虽然Deque的接口有12个之多,但无非就是对容器的两端进行操作,或添加,或删除,或查看。明白了这一点讲解起来就会非常简单。
虽然 Deque 实现没有严格要求禁止插入 null 元素,但建议最好这样做。建议任何事实上允许 null 元素的 Deque 实现用户最好不 要利用插入 null 的功能。这是因为各种方法会将null 用作特殊的返回值来指示双端队列为空。
Deque 实现通常不定义基于元素的 equals 和 hashCode 方法,而是从 Object 类继承基于身份的equals 和 hashCode 方法。
ArrayList 是一个数组队列,相当于 动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。
ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。
ArrayList中的操作不是线程安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList。
首先介绍“回溯”算法的应用。“回溯”算法也叫“回溯搜索”算法,主要用于在一个庞大的空间里搜索我们所需要的问题的解。我们每天使用的“搜索引擎”就是帮助我们在庞大的互联网上搜索我们需要的信息。“搜索”引擎的“搜索”和“回溯搜索”算法的“搜索”意思是一样的。
“回溯”指的是“状态重置”,可以理解为“回到过去”、“恢复现场”,是在编码的过程中,是为了节约空间而使用的一种技巧。而回溯其实是“深度优先遍历”特有的一种现象。之所以是“深度优先遍历”,是因为我们要解决的问题通常是在一棵树上完成的,在这棵树上搜索需要的答案,一般使用深度优先遍历。
“全排列”就是一个非常经典的“回溯”算法的应用。
使用编程的方法得到全排列,就是在这样的一个树形结构中进行编程,具体来说,就是执行一次深度优先遍历,从树的根结点到叶子结点形成的路径就是一个全排列。
说明:
1、每一个结点表示了“全排列”问题求解的不同阶段,这些阶段通过变量的“不同的值”体现;
2、这些变量的不同的值,也称之为“状态”;
3、使用深度优先遍历有“回头”的过程,在“回头”以后,状态变量需要设置成为和先前一样;
4、因此在回到上一层结点的过程中,需要撤销上一次选择,这个操作也称之为“状态重置”;
5、深度优先遍历,可以直接借助系统栈空间,为我们保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈。
6、深度优先遍历通过“回溯”操作,实现了全局使用一份状态变量的效果。
下面我们解释如何编码:
1、首先这棵树除了根结点和叶子结点以外,每一个结点做的事情其实是一样的,即在已经选了一些数的前提,我们需要在剩下还没有选择的数中按照顺序依次选择一个数,这显然是一个递归结构;
2、递归的终止条件是,数已经选够了,因此我们需要一个变量来表示当前递归到第几层,我们把这个变量叫做 depth;
3、这些结点实际上表示了搜索(查找)全排列问题的不同阶段,为了区分这些不同阶段,我们就需要一些变量来记录为了得到一个全排列,程序进行到哪一步了,在这里我们需要两个变量:
(1)已经选了哪些数,到叶子结点时候,这些已经选择的数就构成了一个全排列;
(2)一个布尔数组 used,初始化的时候都为 false 表示这些数还没有被选择,当我们选定一个数的时候,就将这个数组的相应位置设置为 true ,这样在考虑下一个位置的时候,就能够以 O(1)的时间复杂度判断这个数是否被选择过,这是一种“以空间换时间”的思想。
我们把这两个变量称之为“状态变量”,它们表示了我们在求解一个问题的时候所处的阶段。
4、在非叶子结点处,产生不同的分支,这一操作的语义是:在还未选择的数中依次选择一个元素作为下一个位置的元素,这显然得通过一个循环实现。
5、另外,因为是执行深度优先遍历,从较深层的结点返回到较浅层结点的时候,需要做“状态重置”,即“回到过去”、“恢复现场”
这种在遍历的过程中,从深层结点回到浅层结点的过程中所做的操作就叫“回溯”。
在 Java 中,因为都是值传递,对象类型变量在传参的过程中,复制的都是变量的地址。
这些地址被添加到 res 变量,但实际上指向的是同一块内存地址,因此我们会看到 6 个空的列表对象。解决的方法很简单,在 res.add(path); 这里做一次拷贝即可。
回溯算法的时间复杂度一般都比较高,有些问题分析起来很复杂。
1、为什么使用深度优先遍历?
(1)首先是正确性,只有遍历状态空间,才能得到所有符合条件的解;
(2)在深度优先遍历的时候,不同状态之间的切换很容易,可以再看一下上面有很多箭头的那张图,每两个状态之间的差别只有 1 处,因此回退非常方便,这样全局才能使用一份状态变量完成搜索;
(3)如果使用广度优先遍历,从浅层转到深层,状态的变化就很大,此时我们不得不在每一个状态都新建变量去保存它,从性能来说是不划算的;
(4)如果使用广度优先遍历就得使用队列,然后编写结点类。使用深度优先遍历,我们是直接使用了系统栈,系统栈帮助我们保存了每一个结点的状态信息。于是我们不用编写结点类,不必手动编写栈完成深度优先遍历。大家可以尝试使用广度优先遍历实现一下,就能体会到这一点。
2、不回溯可不可以?
可以。搜索问题的状态空间一般很大,如果每一个状态都去创建新的变量,时间复杂度是 O(N)O(N)。在候选数比较多的时候,在非叶子结点上创建新的状态变量的性能消耗就很严重。
就本题而言,只需要叶子结点的那个状态,在叶子结点执行拷贝,时间复杂度是 O(N)O(N)。路径变量在深度优先遍历的时候,结点之间的转换只需要 O(1)O(1)。
最后,由于回溯算法的时间复杂度很高,因此,如果在遍历的时候,如果能够提前知道这一条分支不能搜索到满意的结果,就可以提前结束,这一步操作称之为剪枝。
回溯算法会大量应用“剪枝”技巧达到以加快搜索速度。有些时候,需要做一些预处理工作(例如排序)才能达到剪枝的目的。预处理工作虽然也消耗时间,但一般而且能够剪枝节约的时间更多。还有正是因为回溯问题本身时间复杂度就很高,所以能用空间换时间就尽量使用空间。否则时间消耗又上去了。
一般在纸上画递归树有用。