Java面试题4-数据结构与算法基础

说一下几种常见的排序算法和分别的复杂度

 

倒排一个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。

和thestoryofsnow的博客

http://blog.csdn.net/thestoryofsnow/article/details/6822576

代码来自thefutureisour

http://blog.csdn.net/thefutureisour/article/details/8174313

原文链接:https://blog.csdn.net/duxinyuhi/article/details/53379239

 

龟兔解法的基本思想可以用我们跑步的例子来解释,如果两个人同时出发,如果赛道有环,那么快的一方总能追上慢的一方。进一步想,追上时快的一方肯定比慢的一方多跑了几圈,即多跑的路的长度是圈的长度的倍数。

 

基于上面的想法,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步,即到了环的起点,这是他们第一次相遇的地方。

当他们相遇时,乌龟走的步数就是环的地点在的位置。

求环的长度的问题,第一次相遇后,再次相遇时,走的距离就是环的长度。

 

如何遍历一棵二叉树

 

image.png


 

 


二叉树的深度优先遍历和广度优先遍历

 

广度优先遍历

英文缩写为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的实现方式

 

  1. HashSet概述:

   HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用null元素

 

堆和栈在内存中的区别是什么

 

  • 栈内存存储的是局部变量而堆内存存储的是实体;
  • 栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;
  • 栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收

 

什么是深拷贝和浅拷贝

 

浅拷贝: 是一个传址,也就是把a的值赋给b的时候同时也把a的址赋给了b,当b(a)的值改变的时候,a(b)的值同时也会改变

深拷贝:深拷贝是指,拷贝对象的具体内容,二内存地址是自主分配的,拷贝结束之后俩个对象虽然存的值是一样的,但是内存地址不一样,俩个对象页互相不影响,互不干涉

posted @ 2020-01-20 09:46  reload  阅读(1724)  评论(0编辑  收藏  举报