说一下几种常见的排序算法和分别的复杂度
倒排一个LinkedList
Collecionts.reverse(List<?> list)
什么是跳表
听到跳表(skiplist)这个名字,既然是list,那么应该跟链表有关。
跳表是有序链表,但是我们知道,即使对于排过序的链表,我们对于查找还是需要进行通过链表的指针进行遍历的,时间复杂度很高依然是O(n),这个显然是不能接受的。是否可以像数组那样,通过二分法进行查找呢,但是由于在内存中的存储的不确定性,不能这做。
但是我们可以结合二分法的思想,没错,跳表就是链表与二分法的结合。
1.链表从头节点到尾节点都是有序的
2.可以进行跳跃查找(形如二分法),降低时间复杂度
一个有序的链表,我们选取它的一半的节点用来建索引,这样如果插入一个节点,我们比较的次数就减少了一半。
这种做法,虽然增加了50%的空间,但是性能提高了一倍。
既然,我们已经提取了一层节点索引,那么,可以在第一层索引上再提取索引。
如何确认一个链表有环?进一步,确认环的位置
这是一道很常见的面试问题,,只用两个变量通过O(n)的时间复杂度就可以解决。
Floyd cycle detection算法,也叫做tortoise and hare算法,龟兔算法吧。
http://en.wikipedia.org/wiki/Cycle_detection#Tortoise_and_hare
以下内容为对思路重新梳理了一下,参考了stackoverflow论坛的讨论
http://stackoverflow.com/questions/2936213/explain-how-finding-cycle-start-node-in-cycle-linked-list-work
http://stackoverflow.com/questions/494830/how-to-determine-if-a-linked-list-has-a-cycle-using-only-two-memory-locations。
龟兔解法的基本思想可以用我们跑步的例子来解释,如果两个人同时出发,如果赛道有环,那么快的一方总能追上慢的一方。进一步想,追上时快的一方肯定比慢的一方多跑了几圈,即多跑的路的长度是圈的长度的倍数。
基于上面的想法,Floyd用两个指针,一个慢指针(龟)每次前进一步,快指针(兔)指针每次前进两步(两步或多步效果是等价的,只要一个比另一个快就行,从后面的讨论我们可以看出这一点。ps:考虑一个链表A有1000000个结点的环,链表B是有1000000个的非环的结点,然后再加一个只有3个结点的小环,如果兔子跑的更快点,即每次前进多于2步,那么能更快的检测链表A,但链表B就很慢,因为要一直绕圈等乌龟,所以选择2步是一个tradeoff)。如果两者在链表头以外的某一点相遇(即相等)了,那么说明链表有环,否则,如果(快指针)到达了链表的结尾,那么说明没环。
证明如下:
这样做的道理我用下图解释
假设起点到环的起点距离为m,已经确定有环,环的周长为n,(第一次)相遇点距离环的起点的距离是k。那么当两者相遇时,慢指针移动的总距离为i,因为快指针移动速度为慢指针的两倍,那么快指针的移动距离为2i
1) i = m + p * n + k
2) 2i = m + q * n + ki
其中,p和q分别为慢指针和快指针在第一次相遇时转过的圈数。
2 ( m + p * n + k ) = m + q * n + k
=> 2m + 2pn + 2k = m + nq + k
=> m + k = ( q - 2p ) n
只要有一组p,q,k满足这个式子,假设就成立了。
我们假设
p = 0
q = m
k = m n - m
那么我们证明如下
m + k = ( q - 2p ) n
=> m + mn - m = ( m - 2*0) n
=> mn = mn.
这时,i为
i = m + p n + k
=> m + 0 * n + mn - m = mn.
假设成立。
环的检测
环的检测是Floyd解法的第二部分。
一旦乌龟和兔子相遇,将慢指针移到链表起点,快指针还在他们相遇的地方,即离环的起点距离k的地方。然后慢指针和快指针同时移动,每次移动一步,那么两者再次相遇的地方就是环的起点。
证明如下:
让乌龟和兔子都同时移动m+k步,乌龟到了他们最初遇见的地方(远离环起点k的位置)
之前我们得到 m + k = (q - 2p) n
即兔子走了q-2p圈,也到了和乌龟同样的位置
现在我们不让乌龟走 m+k 步,让乌龟只走 m 步,兔子也后退k步,即到了环的起点,这是他们第一次相遇的地方。
当他们相遇时,乌龟走的步数就是环的地点在的位置。
求环的长度的问题,第一次相遇后,再次相遇时,走的距离就是环的长度。
如何遍历一棵二叉树
二叉树的深度优先遍历和广度优先遍历
广度优先遍历
英文缩写为BFS即Breadth FirstSearch。其过程检验来说是对每一层节点依次访问,访问完一层进入下一层,而且每个节点只能访问一次。对于上面的例子来说,广度优先遍历的 结果是:A,B,C,D,E,F,G,H,I(假设每层节点从左到右访问)。
先往队列中插入左节点,再插右节点,这样出队就是先左节点后右节点了。
广度优先遍历树,需要用到队列(Queue)来存储节点对象,队列的特点就是先进先出。例如,上面这颗树的访问如下:
首先将A节点插入队列中,队列中有元素(A);
将A节点弹出,同时将A节点的左、右节点依次插入队列,B在队首,C在队尾,(B,C),此时得到A节点;
继续弹出队首元素,即弹出B,并将B的左、右节点插入队列,C在队首,E在队尾(C,D,E),此时得到B节点;
继续弹出,即弹出C,并将C节点的左、中、右节点依次插入队列,(D,E,F,G,H),此时得到C节点;
将D弹出,此时D没有子节点,队列中元素为(E,F,G,H),得到D节点;
public class Solution{
private List<Integer> bfs(TreeNode root){
List<Integer> result = new ArrayList<>();
if(root == null) return result;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
TreeNode node = queue.poll();
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
result.add(node.val);
}
return result;
}
}
深度优先遍历
英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。对于上面的例子来说深度优先遍历的结果就是:A,B,D,E,I,C,F,G,H.(假设先走子节点的的左侧)。
深度优先遍历各个节点,需要使用到栈(Stack)这种数据结构。stack的特点是是先进后出。整个遍历过程如下:
先往栈中压入右节点,再压左节点,这样出栈就是先左节点后右节点了。
首先将A节点压入栈中,stack(A);
将A节点弹出,同时将A的子节点C,B压入栈中,此时B在栈的顶部,stack(B,C);
将B节点弹出,同时将B的子节点E,D压入栈中,此时D在栈的顶部,stack(D,E,C);
将D节点弹出,没有子节点压入,此时E在栈的顶部,stack(E,C);
将E节点弹出,同时将E的子节点I压入,stack(I,C);
…依次往下,最终遍历完成。
public class Solution{
private List<Integer> dfs(TreeNode node){
List<Integer> list = new ArrayList<>();
if(root == null) return list;
Stack<Integer> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
if(node.right != null) stack.push(node.right);
if(node.left != null) stack.push(node.left);
result.add(node.val);
}
return result;
}
}
HashSet的实现方式
- HashSet概述:
HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用null元素
堆和栈在内存中的区别是什么
- 栈内存存储的是局部变量而堆内存存储的是实体;
- 栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;
- 栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收
什么是深拷贝和浅拷贝
浅拷贝: 是一个传址,也就是把a的值赋给b的时候同时也把a的址赋给了b,当b(a)的值改变的时候,a(b)的值同时也会改变
深拷贝:深拷贝是指,拷贝对象的具体内容,二内存地址是自主分配的,拷贝结束之后俩个对象虽然存的值是一样的,但是内存地址不一样,俩个对象页互相不影响,互不干涉