一、单链表反转
1、单链表反转问题面试中经常问,而链表这个东西相对于数组的确稍微难想象,因此今天纪录一下单链表反转的代码。
public class Node {
int index;
Node next;
public Node(int index, Node next) {
this.index = index;
this.next = next;
}
}
2,我一共写了三种方法
(1)迭代法。先将下一节点纪录下来,然后让当前节点指向上一节点,再将当前节点纪录下来,再让下一节点变为当前节点
public Node reverse(Node node) {
Node prev = null;
Node now = node;
while (now != null) {
Node next = now.next;
now.next = prev;
prev = now;
now = next;
}
return prev;
}
(2)递归方法1。先找到最后一个节点,然后从最后一个开始反转,然后当前节点反转时其后面的节点已经进行反转了,不需要管。最后返回原来的最后一个节点
public Node reverse2(Node node, Node prev) {
if (node.next == null) {
node.next = prev;
return node;
} else {
Node re = reverse2(node.next, node);
node.next = prev;
return re;
}
}
(3)递归方法2。先找到最后一个节点,然后从最后一个节点之前的那个节点的方法体中开始将下一个指向当前一个,然后当前节点反转时其后面的节点已经进行反转了,不需要管。最后返回原来的最后一个节点。
public Node reverse3(Node node) {
if(node.next==null)return node;
Node next = node.next;
node.next = null;
Node re = reverse3(next);
next.next = node;
return re;
}
总结:迭代法思路很清晰,就是将当前节点和下一节点保存起来,然后将当前节点反转;递归法1是先找到最后一个节点进行反转,然后再反转之前的节点时就不用担心丢失以后的节点了,只需要关心本节点的反转;递归法2是同理,只是反转动作是从最后一个节点的前一个节点开始的。另外这几个方法都没有考虑首节点为null的情况,切记。
二、找到字符串中第一个不重复的元素
题目:在一个字符串中找到第一个没有重复元素的字符并返回。
例:输入:"yellow"
返回:“y”
输入:"tooth"
返回:“h”
输入:“coco”
返回:“”
这个题目我在亚马逊电话面试中遇见过,后来再一家日本公司面试中也遇见过。尽管这个问题并没有涉及到比较高级的编程思想,比如动态编程dynamic Programming或者Divide and Conquer,但是如果对数据结构或者算法并不很熟练的人来说,还是会构成一定的困难。
好了,那么现在就来分析一下这个问题。
首先,最直观的解法就是利用循环挨个儿从第一个元素起往后面找有没有重复,如果遍历完成后还没有,那么这个元素就是第一个没有重复的元素,直接在循环体中返回值就可以了。那么具体解法就需要两个循环控制,第一个用来定位要查看的元素,第二个循环用来在字符串中找有没有跟他重复的元素。这个算法的时间复杂度是O(n2), 因为假设这个字符串中元素都有重复,那么第一个循环需要遍历n次,第二个循环也需要遍历n次。空间复杂度是O(c),为常数,因为我们并没有添加新的数据结构。Java代码如下:
public static String findCharInPlace(String s){
int len = s.length();
if(len <= 0) return null;
boolean repeated = false;
for(int i=0;i<s.length();i++){
repeated = false;
int j=0;
for(;j<s.length();j++){
if(j!=i&&s.charAt(j)==s.charAt(i)){
repeated = true;
break;
}
}
if(!repeated)
return s.charAt(i)+"";
}
return "";
}
那么现在我们来看看有没有第二种解法。我们注意到,在Java中的Map类。我们可以用HashMap映射这个字符串的元素作为key,让每一个元素对应一个重复出现次数的标记,这个标记我们用一个Integer。HashMap<Character, Integer>.我们从头遍历这个字符串,如果后面重复出现了之前出现过的元素,我们就相应这个元素对应的重复出现标记加一。最后在遍历这个Map,找到第一个重复出现标记为一的元素并返回。可这里问题不仅要找到没有重复元素的元素,还要是返回第一个不重复的。但是HashMap的一个问题就是它是无序的。EntrySet并不会按照插入次序来排列,所以怎么办呢。我们知道Java中LinkedList是有序的。所以这里我们就可以用到LinkedHashMap来实现这个功能。 LinkedHashMap结合了LinkedList和HashMap的优点,得到的EntrySet是由一个内部的LinkedList来维护的。所以我们就可以用这个神奇的类来实现这个解法。代码如下:
public static String findChar(String s) {
String ans = null;
Map<Character, Integer> sMap = new LinkedHashMap<Character, Integer>();
for(char c: s.toCharArray()){
if(!sMap.containsKey(c)){
sMap.put(c, 1);
}else{
sMap.put(c, sMap.get(c)+1);
}
}
for(Entry<Character, Integer> en: sMap.entrySet()){
if(en.getValue()==1) return en.getKey().toString();
}
return ans;
}
现在我们来分析一下这个算法的复杂度。LinkedHashMap中add,contail和remove并没有因为有了LinkedList这个特性而增加复杂度,依然是常数级别的复杂度。所以第一个遍历标记的时间复杂度是O(n),第二遍遍历查找的时间复杂度也是O(n),所以整个算法的时间复杂度是O(n).因为我们新添加了LinkedHashMap来对每个字符进行标记,所以空间复杂度是O(n).但从时间上来讲,还是相当快的。
那么我们来分析一下,还有没有更好的实现方法,比如达到O(log(n))的。没有。为什么呢?因为为了判断是否有元素重复,在最坏情况下(比如所有元素都是重复的)我们至少要完全遍历这个字符串一遍才能确定,复杂度为O(n),所以,这个问题的最优解是O(n).
最后让我们来分析对比一下这两个方法的优劣。表面上,单从时间复杂度上来说,算法2无疑是最优的。但是算法2有个问题,是新添加了数据结构LinkedHashMap,所以空间复杂度要高于算法1,这在输入字符串长度不是很大,并且重复度不高,而同时对系统内存资源比较紧俏的情况下来说,算法1有时会比算法2要好一些。因此,在评判一个算法优劣的时候,要结合应用场景来看,而不能单纯从时间复杂度或者空间复杂度上来看。
三、判断一棵二叉树是不是另一棵二叉树的子树
定义:父树包含子树的所有节点,注意,空树不是任何数的子树。
父树:A
子树:B
解法:用递归来实现,从A树的根节点开始,判断其所有的节点是不是依次和树B相同,如不同,递归调用函数,继续判断树A当前节点的左子树的所有节点或右子树的所有节点是否和树B所有节点相同,直到遍历到父树A的叶子节点,如果不是完全相同,则树B不是树A子树,如果直到遍历到树B的叶子节点,其所有节点在树A中均有,则树B是树A的子树。
代码如下:
public class Solution {
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
if(root1==null||root2==null){
return false;
}
return isSubtree(root1,root2)||HasSubtree(root1.left,root2)||HasSubtree(root1.right,root2);
}
public boolean isSubtree(TreeNode root1,TreeNode root2){
if(root2==null){
return true;
}
if(root1==null){
return false;
}
if(root1.val==root2.val){
return isSubtree(root1.left,root2.left)&&isSubtree(root1.right,root2.right);
}
return false;
}
}
[java]使用两个堆栈实现队列功能
具体代码如下:
思路如有错误欢迎指正:
![](https://i-blog.csdnimg.cn/blog_migrate/c0cb6c3dd97fddfc2a15a30adb601fa2.png)
import
java.util.Stack;
/** * 使用两个栈实现队列功能 * 思路: s1 作为入队存储数据的功能 * s2 作为中转件,出队时先把s1中数据取出然后加入到s2中,出队之后再返回到s1中。 *
@author
miaoqiang * */
public
class
TwoStackForQueue
{
private
static
Stack<Object> s1,s2;
static
{ s1 =
new
Stack<Object>(); s2 =
new
Stack<Object>(); }
public
static
boolean
inQueue
(Object obj){
return
s1.add(obj); }
public
static
Stack<Object>
getQueue
(){
return
s1; }
public
static
Object
outQueue
(
int
i){
for
(Object ob:s1){ s2.add(ob); } Object res = s2.remove(i); s1.clear();
if
(res!=
null
){
for
(Object ob:s2){ s1.add(ob); } } s2.clear();
return
res; }
public
static
void
main
(String[] args) {
for
(
int
i=
1
;i<=
10
;i++){ TwoStackForQueue.inQueue(
"test"
+i); } Stack tem = TwoStackForQueue.getQueue(); System.out.println(tem);
for
(
int
i=TwoStackForQueue.getQueue().size()-
1
;i>=
0
;i--){ System.out.println(TwoStackForQueue.getQueue().size()); TwoStackForQueue.outQueue(i); Stack tem2 = TwoStackForQueue.getQueue(); System.out.println(tem); } }}
二叉树是一种非常重要的
数据结构
,非常多其他数据结构都是基于二叉树的基础演变而来的。对于二叉树,有深度遍历和广度遍历,深度遍历有前序、中序以及后序三种遍历方法,广度遍历即我们寻常所说的层次遍历。由于树的定义本身就是递归定义,因此採用递归的方法去实现树的三种遍历不仅easy理解并且代码非常简洁,而对于广度遍历来说,须要其他数据结构的支撑。比方堆了。所以。对于一段代码来说,可读性有时候要比代码本身的效率要重要的多。
四种基本的遍历思想为:
前序遍历:根结点 ---> 左子树 ---> 右子树
中序遍历:左子树---> 根结点 ---> 右子树
后序遍历:左子树 ---> 右子树 ---> 根结点
层次遍历:仅仅需按层次遍历就可以
比如。求以下二叉树的各种遍历
![](https://i-blog.csdnimg.cn/blog_migrate/a18345565ea20e6a611767486650158a.png)
前序遍历:1 2 4 5 7 8 3 6
中序遍历:4 2 7 5 8 1 3 6
后序遍历:4 7 8 5 2 6 3 1
层次遍历:1 2 3 4 5 6 7 8
一、前序遍历
1)依据上文提到的遍历思路:根结点 ---> 左子树 ---> 右子树,非常easy写出递归版本号:
1)依据上文提到的遍历思路:根结点 ---> 左子树 ---> 右子树,非常easy写出递归版本号:
- public void preOrderTraverse1(TreeNode root) {
- if (root != null) {
- System.out.print(root.val+" ");
- preOrderTraverse1(root.left);
- preOrderTraverse1(root.right);
- }
- }
2)如今讨论非递归的版本号:
依据前序遍历的顺序,优先訪问根结点。然后在訪问左子树和右子树。所以。对于随意结点node。第一部分即直接訪问之,之后在推断左子树是否为空,不为空时即反复上面的步骤,直到其为空。若为空。则须要訪问右子树。注意。在訪问过左孩子之后。须要反过来訪问其右孩子。所以,须要栈这样的数据结构的支持。对于随意一个结点node,详细过程例如以下:
a)訪问之,并把结点node入栈。当前结点置为左孩子;
b)推断结点node是否为空,若为空。则取出栈顶结点并出栈,将右孩子置为当前结点;否则反复a)步直到当前结点为空或者栈为空(能够发现栈中的结点就是为了訪问右孩子才存储的)
代码例如以下:
- public void preOrderTraverse2(TreeNode root) {
- LinkedList<TreeNode> stack = new LinkedList<>();
- TreeNode pNode = root;
- while (pNode != null || !stack.isEmpty()) {
- if (pNode != null) {
- System.out.print(pNode.val+" ");
- stack.push(pNode);
- pNode = pNode.left;
- } else { //pNode == null && !stack.isEmpty()
- TreeNode node = stack.pop();
- pNode = node.right;
- }
- }
- }
二、中序遍历
1)依据上文提到的遍历思路:左子树 ---> 根结点 ---> 右子树,非常easy写出递归版本号:
- public void inOrderTraverse1(TreeNode root) {
- if (root != null) {
- inOrderTraverse1(root.left);
- System.out.print(root.val+" ");
- inOrderTraverse1(root.right);
- }
- }
2)非递归实现,有了上面前序的解释,中序也就比較简单了。同样的道理。仅仅只是訪问的顺序移到出栈时。代码例如以下:
- public void inOrderTraverse2(TreeNode root) {
- LinkedList<TreeNode> stack = new LinkedList<>();
- TreeNode pNode = root;
- while (pNode != null || !stack.isEmpty()) {
- if (pNode != null) {
- stack.push(pNode);
- pNode = pNode.left;
- } else { //pNode == null && !stack.isEmpty()
- TreeNode node = stack.pop();
- System.out.print(node.val+" ");
- pNode = node.right;
- }
- }
- }
三、后序遍历
1)依据上文提到的遍历思路:左子树 ---> 右子树 ---> 根结点。非常easy写出递归版本号:
- public void postOrderTraverse1(TreeNode root) {
- if (root != null) {
- postOrderTraverse1(root.left);
- postOrderTraverse1(root.right);
- System.out.print(root.val+" ");
- }
- }
2)
后序遍历的非递归实现是三种遍历方式中最难的一种。由于在后序遍历中,要保证左孩子和右孩子都已被訪问而且左孩子在右孩子前訪问才干訪问根结点,这就为流程的控制带来了难题。以下介绍两种思路。
第一种思路:对于任一结点P,将其入栈,然后沿其左子树一直往下搜索。直到搜索到没有左孩子的结点,此时该结点出如今栈顶,可是此时不能将其出栈并訪问,因此其右孩子还为被訪问。
所以接下来依照同样的规则对其右子树进行同样的处理,当訪问完其右孩子时。该结点又出如今栈顶,此时能够将其出栈并訪问。这样就保证了正确的訪问顺序。能够看出,在这个过程中,每一个结点都两次出如今栈顶,仅仅有在第二次出如今栈顶时,才干訪问它。因此须要多设置一个变量标识该结点是否是第一次出如今栈顶。
![](https://i-blog.csdnimg.cn/blog_migrate/4d24678df4bf12c78505881eac45b68c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/4d24678df4bf12c78505881eac45b68c.png)
void
postOrder2(BinTree *root)
//非递归后序遍历
{
stack<BTNode*> s;
BinTree *p=root;
BTNode *temp;
while
(p!=NULL||!s.empty())
{
while
(p!=NULL)
//沿左子树一直往下搜索。直至出现没有左子树的结点
{
BTNode *btn=(BTNode *)malloc(
sizeof
(BTNode));
btn->btnode=p;
btn->isFirst=
true
;
s.push(btn);
p=p->lchild;
}
if
(!s.empty())
{
temp=s.top();
s.pop();
if
(temp->isFirst==
true
)
//表示是第一次出如今栈顶
{
temp->isFirst=
false
;
s.push(temp);
p=temp->btnode->rchild;
}
else
//第二次出如今栈顶
{
cout<<temp->btnode->data<<
" "
;
p=NULL;
}
}
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/4d24678df4bf12c78505881eac45b68c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/4d24678df4bf12c78505881eac45b68c.png)
另外一种思路:要保证根结点在左孩子和右孩子訪问之后才干訪问,因此对于任一结点P。先将其入栈。假设P不存在左孩子和右孩子。则能够直接訪问它;或者P存在左孩子或者右孩子。可是其左孩子和右孩子都已被訪问过了。则相同能够直接訪问该结点。若非上述两种情况。则将P的右孩子和左孩子依次入栈。这样就保证了每次取栈顶元素的时候,左孩子在右孩子前面被訪问。左孩子和右孩子都在根结点前面被訪问。
![](https://i-blog.csdnimg.cn/blog_migrate/4d24678df4bf12c78505881eac45b68c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/4d24678df4bf12c78505881eac45b68c.png)
void
postOrder3(BinTree *root)
//非递归后序遍历
{
stack<BinTree*> s;
BinTree *cur;
//当前结点
BinTree *pre=NULL;
//前一次訪问的结点
s.push(root);
while
(!s.empty())
{
cur=s.top();
if
((cur->lchild==NULL&&cur->rchild==NULL)||
(pre!=NULL&&(pre==cur->lchild||pre==cur->rchild)))
{
cout<<cur->data<<
" "
;
//假设当前结点没有孩子结点或者孩子节点都已被訪问过
s.pop();
pre=cur;
}
else
{
if
(cur->rchild!=NULL)
s.push(cur->rchild);
if
(cur->lchild!=NULL)
s.push(cur->lchild);
}
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/4d24678df4bf12c78505881eac45b68c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/4d24678df4bf12c78505881eac45b68c.png)
四、层次遍历
层次遍历的代码比較简单。仅仅须要一个队列就可以。先在队列中增加根结点。之后对于随意一个结点来说。在其出队列的时候,訪问之。同一时候假设左孩子和右孩子有不为空的。入队列。代码例如以下:
- public void levelTraverse(TreeNode root) {
- if (root == null) {
- return;
- }
- LinkedList<TreeNode> queue = new LinkedList<>();
- queue.offer(root);
- while (!queue.isEmpty()) {
- TreeNode node = queue.poll();
- System.out.print(node.val+" ");
- if (node.left != null) {
- queue.offer(node.left);
- }
- if (node.right != null) {
- queue.offer(node.right);
- }
- }
- }
五、深度优先遍历
事实上深度遍历就是上面的前序、中序和后序。可是为了保证与广度优先遍历相照顾,也写在这。代码也比較好理解,事实上就是前序遍历,代码例如以下:
- public void depthOrderTraverse(TreeNode root) {
- if (root == null) {
- return;
- }
- LinkedList<TreeNode> stack = new LinkedList<>();
- stack.push(root);
- while (!stack.isEmpty()) {
- TreeNode node = stack.pop();
- System.out.print(node.val+" ");
- if (node.right != null) {
- stack.push(node.right);
- }
- if (node.left != null) {
- stack.push(node.left);
- }
- }
- }
寻找二叉树的最大深度
/**
* Definition for binary tree
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Solution {
public int maxDepth(TreeNode root) {
if(root == null)
return 0;
else if(root.left == null && root.right == null)
return 1;
else
{
int leftDepth= maxDepth(root.left);
int rightDepth = maxDepth(root.right);
if(leftDepth > rightDepth)
return leftDepth +1;
else
return rightDepth +1;
}
}
}
java字符串反转,逆序输出(句子反转,单词不反转)
class String2Test
{
public static void main( String[] args )
{
if(args.length<1)
{
System.out.println("Error! ---Need parameter");
System.exit(-1);
}
StringBuffer s1 = new StringBuffer(); /* obtain original string */
StringBuffer s2 = new StringBuffer(); /* save the reverse string */
StringBuffer buffer = new StringBuffer(); /* the buffer for saving word */
s1.append(args[0]);
byte symbol; /* record the status, reading char or space */
char[] chars = new char[s1.length()]; /* the array for saving string to process */
s1.getChars(0,s1.length(),chars,0);
if(chars[chars.length-1]==' ' || chars[chars.length-1]=='\t')
symbol=0;
else
symbol=1;
/** scan and reverse the string **/
for( int i=(chars.length-1); i>=0; i--)
{
if(chars[i]!=' ' && chars[i]!='\t' )
{
if( symbol==0 )
{
symbol=1;
buffer.delete(0,buffer.length());
}
buffer.append(chars[i]);
}
else if( symbol==1)
{
symbol=0;
System.out.println(buffer.reverse());
s2.append(buffer);
s2.append(chars[i]);
}
}
/** process the head of the string **/
if(symbol==1)
{
System.out.println(buffer.reverse());
s2.append(buffer);
}
System.out.println(s2);
}
}
题目:
在排序数组中,找出给定元素出现的次数。
例如:有序数组[1,2,3, 4, 5, 5, 5, 5,6,7,8]中,5出现的次数为4次。
1.直接比较统计,O(N)的时间复杂度。
int findCount(int a[],int len ,int key){
int i,count = 0;
for(i=0;i<len;i++)
{
if(key==a[i])
count++;
}
return count;
}
2.利用二分查找,分别找出最先出现和最后出现的位置,再统计出现的次数即可,时间复杂度为O(logN)。
int BinarySort(int a[],int len, int key, bool isLeft)
{
int left = 0, right = len -1;
int last = 0 ; // 记录下标
while(left<=right)
{
int mid = (left + right)/2;
if(a[mid]<key)
{
left = mid + 1;
}
else if(a[mid]>key)
{
right = mid -1;
}
else
{
last = mid ;
if(isLeft) // 该值在mid左边还有时
{
right = mid - 1;
}
else
{
left = mid + 1;
}
}
}
return last>0?last:-1;
}
替换字符串中的字符,全部替换成指定字符串,然后问了一些链表判断是否为循环链表,寻找循环节点在哪里。
java用递归编程求斐波那契数列第n项
public static int function(int n){
if(n==1 || n==2) return 1;
return function(n-1)+function(n-2);
}
编程实现一个阻塞队列。
大数据排序或取重或去重相关问题
1. 给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?
方案1:可以估计每个文件安的大小为50G×64=320G,远远大于内存限制的4G。所以不可能将其完全加载到内存中处理。考虑采取分而治之的方法。
s 遍历文件a,对每个url求取 ,然后根据所取得的值将url分别存储到1000个小文件(记为 )中。这样每个小文件的大约为300M。
s 遍历文件b,采取和a相同的方式将url分别存储到1000各小文件(记为 )。这样处理后,所有可能相同的url都在对应的小文件( )中,不对应的小文件不可能有相同的url。然后我们只要求出1000对小文件中相同的url即可。
s 求每对小文件中相同的url时,可以把其中一个小文件的url存储到hash_set中。然后遍历另一个小文件的每个url,看其是否在刚才构建的hash_set中,如果是,那么就是共同的url,存到文件里面就可以了。
方案2:如果允许有一定的错误率,可以使用Bloom filter,4G内存大概可以表示340亿bit。将其中一个文件中的url使用Bloom filter映射为这340亿bit,然后挨个读取另外一个文件的url,检查是否与Bloom filter,如果是,那么该url应该是共同的url(注意会有一定的错误率)。
2. 有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求你按照query的频度排序。
方案1:
s 顺序读取10个文件,按照hash(query)%10的结果将query写入到另外10个文件(记为 )中。这样新生成的文件每个的大小大约也1G(假设hash函数是随机的)。
s 找一台内存在2G左右的机器,依次对 用hash_map(query, query_count)来统计每个query出现的次数。利用快速/堆/归并排序按照出现次数进行排序。将排序好的query和对应的query_cout输出到文件中。这样得到了10个排好序的文件(记为 )。
s 对 这10个文件进行归并排序(内排序与外排序相结合)。
方案2:
一般query的总量是有限的,只是重复的次数比较多而已,可能对于所有的query,一次性就可以加入到内存了。这样,我们就可以采用trie树/hash_map等直接来统计每个query出现的次数,然后按出现次数做快速/堆/归并排序就可以了。
方案3:
与方案1类似,但在做完hash,分成多个文件后,可以交给多个文件来处理,采用分布式的架构来处理(比如MapReduce),最后再进行合并。
3. 有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
方案1:顺序读文件中,对于每个词x,取 ,然后按照该值存到5000个小文件(记为 ) 中。这样每个文件大概是200k左右。如果其中的有的文件超过了1M大小,还可以按照类似的方法继续往下分,知道分解得到的小文件的大小都不超过1M。对 每个小文件,统计每个文件中出现的词以及相应的频率(可以采用trie树/hash_map等),并取出出现频率最大的100个词(可以用含100个结点 的最小堆),并把100词及相应的频率存入文件,这样又得到了5000个文件。下一步就是把这5000个文件进行归并(类似与归并排序)的过程了。
4. 海量日志数据,提取出某日访问百度次数最多的那个IP。
方案1:首先是这一天,并且是访问百度的日志中的IP取出来,逐个写入到一个大文件中。注意到IP是32位的,最多有 个 IP。同样可以采用映射的方法,比如模1000,把整个大文件映射为1000个小文件,再找出每个小文中出现频率最大的IP(可以采用hash_map进 行频率统计,然后再找出频率最大的几个)及相应的频率。然后再在这1000个最大的IP中,找出那个频率最大的IP,即为所求。
5. 在2.5亿个整数中找出不重复的整数,内存不足以容纳这2.5亿个整数。
方案1:采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)进行,共需内存 内存,还可以接受。然后扫描这2.5亿个整数,查看Bitmap中相对应位,如果是00变01,01变10,10保持不变。所描完事后,查看bitmap,把对应位是01的整数输出即可。
方案2:也可采用上题类似的方法,进行划分小文件的方法。然后在小文件中找出不重复的整数,并排序。然后再进行归并,注意去除重复的元素。
6. 海量数据分布在100台电脑中,想个办法高校统计出这批数据的TOP10。
方案1:
s 在每台电脑上求出TOP10,可以采用包含10个元素的堆完成(TOP10小,用最大堆,TOP10大,用最小堆)。比如求TOP10大,我们首先取前 10个元素调整成最小堆,如果发现,然后扫描后面的数据,并与堆顶元素比较,如果比堆顶元素大,那么用该元素替换堆顶,然后再调整为最小堆。最后堆中的元 素就是TOP10大。
s 求出每台电脑上的TOP10后,然后把这100台电脑上的TOP10组合起来,共1000个数据,再利用上面类似的方法求出TOP10就可以了。
7. 怎么在海量数据中找出重复次数最多的一个?
方案1:先做hash,然后求模映射为小文件,求出每个小文件中重复次数最多的一个,并记录重复次数。然后找出上一步求出的数据中重复次数最多的一个就是所求(具体参考前面的题)。
8. 上千万或上亿数据(有重复),统计其中出现次数最多的钱N个数据。
方案1:上千万或上亿的数据,现在的机器的内存应该能存下。所以考虑采用hash_map/搜索二叉树/红黑树等来进行统计次数。然后就是取出前N个出现次数最多的数据了,可以用第6题提到的堆机制完成。
9. 1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。请怎么设计和实现?
方案1:这题用trie树比较合适,hash_map也应该能行。
10. 一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。
方案1:这题是考虑时间效率。用trie树统计每个词出现的次数,时间复杂度是O(n*le)(le表示单词的平准长度)。然后是找出出现最频繁的 前10个词,可以用堆来实现,前面的题中已经讲到了,时间复杂度是O(n*lg10)。所以总的时间复杂度,是O(n*le)与O(n*lg10)中较大 的哪一个。
11. 一个文本文件,找出前10个经常出现的词,但这次文件比较长,说是上亿行或十亿行,总之无法一次读入内存,问最优解。
方案1:首先根据用hash并求模,将文件分解为多个小文件,对于单个文件利用上题的方法求出每个文件件中10个最常出现的词。然后再进行归并处理,找出最终的10个最常出现的词。
12. 100w个数中找出最大的100个数。
方案1:在前面的题中,我们已经提到了,用一个含100个元素的最小堆完成。复杂度为O(100w*lg100)。
方案2:采用快速排序的思想,每次分割之后只考虑比轴大的一部分,知道比轴大的一部分在比100多的时候,采用传统排序算法排序,取前100个。复杂度为O(100w*100)。
方案3:采用局部淘汰法。选取前100个元素,并排序,记为序列L。然后一次扫描剩余的元素x,与排好序的100个元素中最小的元素比,如果比这个 最小的要大,那么把这个最小的元素删除,并把x利用插入排序的思想,插入到序列L中。依次循环,知道扫描了所有的元素。复杂度为O(100w*100)。
13. 寻找热门查询:
搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复 读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个 查询串,要求使用的内存不能超过1G。
(1) 请描述你解决这个问题的思路;
(2) 请给出主要的处理流程,算法,以及算法的复杂度。
方案1:采用trie树,关键字域存该查询串出现的次数,没有出现为0。最后用10个元素的最小推来对出现频率进行排序。
14. 一共有N个机器,每个机器上有N个数。每个机器最多存O(N)个数并对它们操作。如何找到 个数中的中数?
方案1:先大体估计一下这些数的范围,比如这里假设这些数都是32位无符号整数(共有 个)。我们把0到 的整数划分为N个范围段,每个段包含 个整数。比如,第一个段位0到 ,第二段为 到 ,…,第N个段为 到 。 然后,扫描每个机器上的N个数,把属于第一个区段的数放到第一个机器上,属于第二个区段的数放到第二个机器上,…,属于第N个区段的数放到第N个机器上。 注意这个过程每个机器上存储的数应该是O(N)的。下面我们依次统计每个机器上数的个数,一次累加,直到找到第k个机器,在该机器上累加的数大于或等于 ,而在第k-1个机器上的累加数小于 ,并把这个数记为x。那么我们要找的中位数在第k个机器中,排在第 位。然后我们对第k个机器的数排序,并找出第 个数,即为所求的中位数。复杂度是 的。
方案2:先对每台机器上的数进行排序。排好序后,我们采用归并排序的思想,将这N个机器上的数归并起来得到最终的排序。找到第n个便是所求。复杂度是n(i)的。
15. 最大间隙问题
给定n个实数 ,求着n个实数在实轴上向量2个数之间的最大差值,要求线性的时间算法。
方案1:最先想到的方法就是先对这n个数据进行排序,然后一遍扫描即可确定相邻的最大间隙。但该方法不能满足线性时间的要求。故采取如下方法:
s 找到n个数据中最大和最小数据max和min。
s 用n-2个点等分区间[min, max],即将[min, max]等分为n-1个区间(前闭后开区间),将这些区间看作桶,编号为 ,且桶 的上界和桶i+1的下届相同,即每个桶的大小相同。每个桶的大小为: 。实际上,这些桶的边界构成了一个等差数列(首项为min,公差为 ),且认为将min放入第一个桶,将max放入第n-1个桶。
s 将n个数放入n-1个桶中:将每个元素 分配到某个桶(编号为index),其中 ,并求出分到每个桶的最大最小数据。
s 最大间隙:除最大最小数据max和min以外的n-2个数据放入n-1个桶中,由抽屉原理可知至少有一个桶是空的,又因为每个桶的大小相同,所以最大间隙 不会在同一桶中出现,一定是某个桶的上界和气候某个桶的下界之间隙,且该量筒之间的桶(即便好在该连个便好之间的桶)一定是空桶。也就是说,最大间隙在桶 i的上界和桶j的下界之间产生 。一遍扫描即可完成。
16. 将多个集合合并成没有交集的集合:给定一个字符串的集合,格式如: 。要求将其中交集不为空的集合合并,要求合并完成的集合之间无交集,例如上例应输出 。
(1) 请描述你解决这个问题的思路;
(2) 给出主要的处理流程,算法,以及算法的复杂度;
(3) 请描述可能的改进。
方案1:采用并查集。首先所有的字符串都在单独的并查集中。然后依扫描每个集合,顺序合并将两个相邻元素合并。例如,对于 , 首先查看aaa和bbb是否在同一个并查集中,如果不在,那么把它们所在的并查集合并,然后再看bbb和ccc是否在同一个并查集中,如果不在,那么也把 它们所在的并查集合并。接下来再扫描其他的集合,当所有的集合都扫描完了,并查集代表的集合便是所求。复杂度应该是O(NlgN)的。改进的话,首先可以 记录每个节点的根结点,改进查询。合并的时候,可以把大的和小的进行合,这样也减少复杂度。
17. 最大子序列与最大子矩阵问题
数组的最大子序列问题:给定一个数组,其中元素有正,也有负,找出其中一个连续子序列,使和最大。
方案1:这个问题可以动态规划的思想解决。设 表示以第i个元素 结尾的最大子序列,那么显然 。基于这一点可以很快用代码实现。
最大子矩阵问题:给定一个矩阵(二维数组),其中数据有大有小,请找一个子矩阵,使得子矩阵的和最大,并输出这个和。
方案1:可以采用与最大子序列类似的思想来解决。如果我们确定了选择第i列和第j列之间的元素,那么在这个范围内,其实就是一个最大子序列问题。如何确定第i列和第j列可以词用暴搜的方法进行。
在很多的笔试和面试中,喜欢考察Top K.下面从自身的经验给出三种实现方式及实用范围。
- 合并法
这种方法适用于几个数组有序的情况,来求Top k。时间复杂度为O(k*m)。(m:为数组的个数).具体实现如下:
![复制代码](https://i-blog.csdnimg.cn/blog_migrate/22134163c65e08d29d02fb5a7bdca0c6.png)
复制代码
/*** 已知几个递减有序的m个数组,求这几个数据前k大的数*适合采用Merge的方法,时间复杂度(O(k*m);*/
import
java.util.List;
import
java.util.Arrays;
import
java.util.ArrayList;
public
class
TopKByMerge{
public
int
[] getTopK(List<List<Integer>>input,
int
k){
int
index[]=
new
int
[input.size()];
//保存每个数组下标扫描的位置;
int
result[]=
new
int
[k];
for
(
int
i=0;i<k;i++){
int
max=Integer.MIN_VALUE;
int
maxIndex=0;
for
(
int
j=0;j<input.size();j++){
if
(index[j]<input.get(j).size()){
if
(max<input.get(j).get(index[j])){ max=input.get(j).get(index[j]); maxIndex=j; } } }
if
(max==Integer.MIN_VALUE){
return
result; } result[i]=max; index[maxIndex]+=1; }
return
result; }
![复制代码](https://i-blog.csdnimg.cn/blog_migrate/22134163c65e08d29d02fb5a7bdca0c6.png)
复制代码
- 快排过程法
快排过程法利用快速排序的过程来求Top k.平均时间复杂度为(O(n)).适用于无序单个数组。具体java实现如下:
![复制代码](https://i-blog.csdnimg.cn/blog_migrate/22134163c65e08d29d02fb5a7bdca0c6.png)
复制代码
/**利用快速排序的过程来求最小的k个数**/
public
class
TopK{
int
partion(
int
a[],
int
first,
int
end){
int
i=first;
int
main=a[end];
for
(
int
j=first;j<end;j++){
if
(a[j]<main){
int
temp=a[j]; a[j]=a[i]; a[i]=temp; i++; } } a[end]=a[i]; a[i]=main;
return
i; }
void
getTopKMinBySort(
int
a[],
int
first,
int
end,
int
k){
if
(first<end){
int
partionIndex=partion(a,first,end);
if
(partionIndex==k-
1
)
return
;
else
if
(partionIndex>k-
1
)getTopKMinBySort(a,first,partionIndex-
1
,k);
else
getTopKMinBySort(a,partionIndex+
1
,end,k); } }
public
static
void
main(String []args){
int
a[]={
2
,
20
,
3
,
7
,
9
,
1
,
17
,
18
,
0
,
4
};
int
k=
6
;
new
TopK().getTopKMinBySort(a,
0
,a.length-
1
,k);
for
(
int
i=
0
;i<k;i++){ System.
out
.print(a[i]+
"
"
); } }}
![复制代码](https://i-blog.csdnimg.cn/blog_migrate/22134163c65e08d29d02fb5a7bdca0c6.png)
复制代码
- 采用小根堆或者大根堆
求最大K个采用小根堆,而求最小K个采用大根堆。
求最大K个的步奏:
- 根据数据前K个建立K个节点的小根堆。
- 在后面的N-K的数据的扫描中,
- 如果数据大于小根堆的根节点,则根节点的值覆为该数据,并调节节点至小根堆。
- 如果数据小于或等于小根堆的根节点,小根堆无变化。
求最小K个跟这求最大K个类似。时间复杂度O(nlogK)(n:数据的长度),特别适用于大数据的求Top K。
![复制代码](https://i-blog.csdnimg.cn/blog_migrate/22134163c65e08d29d02fb5a7bdca0c6.png)
复制代码
/** * 求前面的最大K个 解决方案:小根堆 (数据量比较大(特别是大到内存不可以容纳)时,偏向于采用堆) * * */
public
class
TopK {
/** * 创建k个节点的小根堆 * *
@param
a *
@param
k *
@return
*/
int
[] createHeap(
int
a[],
int
k) {
int
[] result =
new
int
[k];
for
(
int
i = 0; i < k; i++) { result[i] = a[i]; }
for
(
int
i = 1; i < k; i++) {
int
child = i;
int
parent = (i - 1) / 2;
int
temp = a[i];
while
(parent >= 0 &&child!=0&& result[parent] >temp) { result[child] = result[parent]; child = parent; parent = (parent - 1) / 2; } result[child] = temp; }
return
result; }
void
insert(
int
a[],
int
value) { a[0]=value;
int
parent=0;
while
(parent<a.length){
int
lchild=2*parent+1;
int
rchild=2*parent+2;
int
minIndex=parent;
if
(lchild<a.length&&a[parent]>a[lchild]){ minIndex=lchild; }
if
(rchild<a.length&&a[minIndex]>a[rchild]){ minIndex=rchild; }
if
(minIndex==parent){
break
; }
else
{
int
temp=a[parent]; a[parent]=a[minIndex]; a[minIndex]=temp; parent=minIndex; } } }
int
[] getTopKByHeap(
int
input[],
int
k) {
int
heap[] =
this
.createHeap(input, k);
for
(
int
i=k;i<input.length;i++){
if
(input[i]>heap[0]){
this
.insert(heap, input[i]); } }
return
heap; }
public
static
void
main(String[] args) {
int
a[] = { 4, 3, 5, 1, 2,8,9,10};
int
result[] =
new
TopK().getTopKByHeap(a, 3);
for
(
int
temp : result) { System.out.println(temp); } }}
在100G文件中找出出现次数最多的100个IP
搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门),请你统计最热门的10个查询串,要求使用的内存不能超过1G。
解答:
我们知道,数据大则划为小的,如如一亿个Ip求Top 10,可先%1000将ip分到1000个小文件中去,并保证一种ip只出现在一个文件中,再对每个小文件中的ip进行hashmap计数统计并按数量排序,最后归并或者最小堆依次处理每个小文件的top10以得到最后的结。
但如果数据规模比较小,能一次性装入内存呢?比如这第2题,虽然有一千万个Query,但是由于重复度比较高,因此事实上只有300万的Query,每个Query255Byte,因此我们可以考虑把他们都放进内存中去(300万个字符串假设没有重复,都是最大长度,那么最多占用内存3M*1K/4=0.75G。所以可以将所有字符串都存放在内存中进行处理),而现在只是需要一个合适的数据结构,在这里,HashTable绝对是我们优先的选择。
所以我们放弃分而治之/hash映射的步骤,直接上hash统计,然后排序。So,针对此类
典型的TOP K问题,采取的对策往往是:hashmap + 堆
。如下所示:
- hash_map统计:先对这批海量数据预处理。具体方法是:维护一个Key为Query字串,Value为该Query出现次数的HashTable,即hash_map(Query,Value),每次读取一个Query,如果该字串不在Table中,那么加入该字串,并且将Value值设为1;如果该字串在Table中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度内用Hash表完成了统计;
- 堆排序:第二步、借助堆这个数据结构,找出Top K,时间复杂度为N‘logK。即借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比。所以,我们最终的时间复杂度是:O(N) + N' * O(logK),(N为1000万,N’为300万)。
接下来再来说说我遇到的这个题,几乎就按这个思路来就好了:
对于100G的文件,先算算能有多少条IP呢?每条IP最长为15个字节,则100G/15=6.7G条,IP一共有多少种呢,不考虑IPv6,约有256^4=2^32条=4G条,那么最极端的情况是每种IP都有,每个都出现那么一两条。
要解决该问题首先要找到一种分类方式,把重复出现的IP都放到一个文件里面,一共分成100份,这可以通过把IP对100取模得到,具体方法如去掉IP中的点转化为一个long型变量,这样取模为0,1,2...99的IP都分到一个文件了,那么这个分就能保证每一文件都能载入内存吗?这可不一定,万一模为9的IP特别多怎么办,可以再对这一类IP做一次取模,直到每个小文件足够载入内存为止。这个分类很关键,如果是随便分成100份,相同的IP被分在了不同的文件中,接下来再对每个文件统计次数并做归并,这个思路就没有意义了,起不到“
大而化小,各个击破,缩小规模,逐个解决
”的效果了。
好了,接下来把每个小文件载入内存,建立哈希表unordered_map<string,int>,将每个IP作为关键字映射为出现次数,这个哈希表建好之后也得先写入硬盘,因为内存就那么多,一共要统计100个文件呢。
在统计完100个文件之后,我再建立一个小顶堆,大小为100,把建立好并存在硬盘哈希表载入内存,逐个对出现次数排序,挑出出现次数最多的100个,由于次数直接和IP是对应的,找出最多的次数也就找出了相应的IP。
这只是个大致的算法,还有些细节,比如第90到110大的元素出现次数一样呢,就随机舍弃掉10个吗?整个的时间复杂度分类O(n),建哈希表O(n),挑出出现最多次数的O(nlogk)