Ⅰ 算法面试
我在《再谈“我是怎么招程序员”》中比较保守地说过,“问难的算法题并没有错,错的很多面试官只是在肤浅甚至错误地理解着面试算法题的目的。”,今天,我想加强一下这个观点——我反对纯算法题面试!(注意,我说的是纯算法题)图片源Wikipedia(点击图片查看词条)我再次引用我以前的一个观点——能解算法题并不意味着这个人就有能力就能在工作中解决问题,你可以想想,小学奥数题可能比这些题更难,但并不意味着那些奥数能手就能解决实际问题。好了,让我们来看一个示例(这个示例是昨天在微博上的一个讨论),这个题是——“找出无序数组中第2大的数”,几乎所有的人都用了O(n)的算法,我相信对于我们这些应试教育出来的人来说,不用排序用O(n)算法是很正常的事,连我都不由自主地认为O(n)算法是这个题的标准答案。我们太习惯于标准答案了,这是我国教育最悲哀的地方。(广义的洗脑就是让你的意识依赖于某个标准答案,然后通过给你标准答案让你不会思考而控制你)功能性需求分析试想,如果我们在实际工作中得到这样一个题 我们会怎么做?我一定会分析这个需求,因为我害怕需求未来会改变,今天你叫我找一个第2大的数,明天你找我找一个第4大的数,后天叫我找一个第100大的数,我不搞死了。需求变化是很正常的事。分析完这个需求后,我会很自然地去写找第K大数的算法——难度一下子就增大了。很多人会以为找第K大的需求是一种“过早扩展”的思路,不是这样的,我相信我们在实际编码中写过太多这样的程序了,你一定不会设计出这样的函数接口 —— Find2ndMaxNum(int* array, int len),就好像你不会设计出 DestroyBaghdad(); 这样的接口,而是设计一个DestoryCity( City& ); 的接口,而把Baghdad当成参数传进去!所以,你应该是声明一个叫FindKthMaxNum(int* array, int len, int kth),把2当成参数传进去。这是最基本的编程方法,用数学的话来说,叫代数!最简单的需求分析方法就是把需求翻译成函数名,然后看看是这个接口不是很二?!(注:不要纠结于FindMaxNum()或FindMinNum(),因为这两个函数名的业务意义很清楚了,不像Find2ndMaxNum()那么二)非功能性需求分析性能之类的东西从来都是非功能性需求,对于算法题,我们太喜欢研究算法题的空间和时间复杂度了。我们希望做到空间和时间双丰收,这是算法学术界的风格。所以,习惯于标准答案的我们已经失去思考的能力,只会机械地思考算法之内的性能,而忽略了算法之外的性能。如果题目是——“从无序数组中找到第K个最大的数”,那么,我们一定会去思考用O(n)的线性算法找出第K个数。事实上,也有线性算法——STL中可以用nth_element求得类似的第n大的数,其利用快速排序的思想,从数组S中随机找出一个元素X,把数组分为两部分Sa和Sb。Sa中的元素大于等于X,Sb中元素小于X。这时有两种情况:1)Sa中元素的个数小于k,则Sb中的第 k-|Sa|个元素即为第k大数;2) Sa中元素的个数大于等于k,则返回Sa中的第k大数。时间复杂度近似为O(n)。搞学术的nuts们到了这一步一定会欢呼胜利!但是他们哪里能想得到性能的需求分析也是来源自业务的!我们一说性能,基本上是个人都会问,请求量有多大?如果我们的FindKthMaxNum()的请求量是m次,那么你的这个每次都要O(n)复杂度的算法得到的效果就是O(n*m),这一点,是书呆子式的学院派人永远想不到的。因为应试教育让我们不会从实际思考了。工程式的解法根据上面的需求分析,有软件工程经验的人的解法通常会这样:1)把数组排序,从大到小。2)于是你要第k大的数,就直接访问 array[k]。排序只需要一次,O(n*log(n)),然后,接下来的m次对FindKthMaxNum()的调用全是O(1)的,整体复杂度反而成了线性的。其实,上述的还不是工程式的最好的解法,因为,在业务中,那数组中的数据可能会是会变化的,所以,如果是用数组排序的话,有数据的改动会让我重新排序,这个太耗性能了,如果实际情况中会有很多的插入或删除操作,那么可以考虑使用B+树。工程式的解法有以下特点:1)很方便扩展,因为数据排好序了,你还可以方便地支持各种需求,如从第k1大到k2大的数据(那些学院派写出来的代码在拿到这个需求时又开始挠头苦想了)2)规整的数据会简化整体的算法复杂度,从而整体性能会更好。(公欲善其事,必先利其器)3)代码变得清晰,易懂,易维护!(学院派的和STL一样的近似O(n)复杂度的算法没人敢动)争论你可能会和我有以下争论,如果程序员做这个算法题用排序的方式,他一定不会像你想那么多。是的,你说得对。但是我想说,很多时候,我们直觉地思考,恰恰是正确的路。因为“排序”这个思路符合人类大脑处理问题的方式,而使用学院派的方式是反大脑直觉的。反大脑直觉的,通常意味着晦涩难懂,维护成本上升。就是一道面试题,我就是想测试一下你的算法技能,这也扯太多了。没问题,不过,我们要清楚我们是在招什么人?是一个只会写算法的人,还是一个会做软件的人?这个只有你自己最清楚。这个算法题太容易诱导到学院派的思路了。是的这道“找出第K大的数”,其实可以变换为更为业务一点的题目——“我要和别的商户竞价,我想排在所有竞争对手报价的第K名,请写一个程序,我输入K,和一个商品名,系统告诉我应该订多少价?(商家的所有商品的报价在一数组中)”——业务分析,整体性能,算法,数据结构,增加需求让应聘者重构,这一个问题就全考了。你是不是在说算法不重要,不用学?千万别这样理解我,搞得好像如果面试不面,我就可以不学。算法很重要,算法题能锻炼我们的思维,而且也有很多实际用处。我这篇文章不是让大家不要去学算法,这是完全错误的,我是让大家带着业务问题去使用算法。问你业务问题,一样会问到算法题上来。小结看过这上面的分析,我相信你明白我为什么反对纯算法面试题了。原因就是纯算法的面试题根本不能反应一个程序的综合素质!那么,在面试中,我们应该要考量程序员的那些综合素质呢?我以为有下面这些东西:会不会做需求分析?怎么理解问题的?解决问题的思路是什么?想法如何?会不会对基础的算法和数据结构灵活运用?另外,我们知道,对于软件开发来说,在工程上,难是的下面是这些挑战:软件的维护成本远远大于软件的开发成本。软件的质量变得越来越重要,所以,测试工作也变得越来越重要。软件的需求总是在变的,软件的需求总是一点一点往上加的。程序中大量的代码都是在处理一些错误的或是不正常的流程。所以,对于编程能力上,我们应该主要考量程序员的如下能力:设计是否满足对需求的理解,并可以应对可能出现的需求变化。
Ⅱ java算法面试题:排序都有哪几种方法
一、冒泡排序
[java] view plain
package sort.bubble;
import java.util.Random;
/**
* 依次比较相邻的两个数,将小数放在前面,大数放在后面
* 冒泡排序,具有稳定性
* 时间复杂度为O(n^2)
* 不及堆排序,快速排序O(nlogn,底数为2)
* @author liangge
*
*/
public class Main {
public static void main(String[] args) {
Random ran = new Random();
int[] sort = new int[10];
for(int i = 0 ; i < 10 ; i++){
sort[i] = ran.nextInt(50);
}
System.out.print("排序前的数组为");
for(int i : sort){
System.out.print(i+" ");
}
buddleSort(sort);
System.out.println();
System.out.print("排序后的数组为");
for(int i : sort){
System.out.print(i+" ");
}
}
/**
* 冒泡排序
* @param sort
*/
private static void buddleSort(int[] sort){
for(int i=1;i<sort.length;i++){
for(int j=0;j<sort.length-i;j++){
if(sort[j]>sort[j+1]){
int temp = sort[j+1];
sort[j+1] = sort[j];
sort[j] = temp;
}
}
}
}
}
二、选择排序
[java] view plain
package sort.select;
import java.util.Random;
/**
* 选择排序
* 每一趟从待排序的数据元素中选出最小(或最大)的一个元素,
* 顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。
* 选择排序是不稳定的排序方法。
* @author liangge
*
*/
public class Main {
public static void main(String[] args) {
Random ran = new Random();
int[] sort = new int[10];
for (int i = 0; i < 10; i++) {
sort[i] = ran.nextInt(50);
}
System.out.print("排序前的数组为");
for (int i : sort) {
System.out.print(i + " ");
}
selectSort(sort);
System.out.println();
System.out.print("排序后的数组为");
for (int i : sort) {
System.out.print(i + " ");
}
}
/**
* 选择排序
* @param sort
*/
private static void selectSort(int[] sort){
for(int i =0;i<sort.length-1;i++){
for(int j = i+1;j<sort.length;j++){
if(sort[j]<sort[i]){
int temp = sort[j];
sort[j] = sort[i];
sort[i] = temp;
}
}
}
}
}
三、快速排序
[java] view plain
package sort.quick;
/**
* 快速排序 通过一趟排序将要排序的数据分割成独立的两部分, 其中一部分的所有数据都比另外一部分的所有数据都要小,
* 然后再按此方法对这两部分数据分别进行快速排序, 整个排序过程可以递归进行,以此达到整个数据变成有序序列。
* @author liangge
*
*/
public class Main {
public static void main(String[] args) {
int[] sort = { 54, 31, 89, 33, 66, 12, 68, 20 };
System.out.print("排序前的数组为:");
for (int data : sort) {
System.out.print(data + " ");
}
System.out.println();
quickSort(sort, 0, sort.length - 1);
System.out.print("排序后的数组为:");
for (int data : sort) {
System.out.print(data + " ");
}
}
/**
* 快速排序
* @param sort 要排序的数组
* @param start 排序的开始座标
* @param end 排序的结束座标
*/
public static void quickSort(int[] sort, int start, int end) {
// 设置关键数据key为要排序数组的第一个元素,
// 即第一趟排序后,key右边的数全部比key大,key左边的数全部比key小
int key = sort[start];
// 设置数组左边的索引,往右移动判断比key大的数
int i = start;
// 设置数组右边的索引,往左移动判断比key小的数
int j = end;
// 如果左边索引比右边索引小,则还有数据没有排序
while (i < j) {
while (sort[j] > key && j > start) {
j--;
}
while (sort[i] < key && i < end) {
i++;
}
if (i < j) {
int temp = sort[i];
sort[i] = sort[j];
sort[j] = temp;
}
}
// 如果左边索引比右边索引要大,说明第一次排序完成,将sort[j]与key对换,
// 即保持了key左边的数比key小,key右边的数比key大
if (i > j) {
int temp = sort[j];
sort[j] = sort[start];
sort[start] = temp;
}
//递归调用
if (j > start && j < end) {
quickSort(sort, start, j - 1);
quickSort(sort, j + 1, end);
}
}
}
[java] view plain
/**
* 快速排序
*
* @param a
* @param low
* @param high
* voidTest
*/
public static void kuaisuSort(int[] a, int low, int high)
{
if (low >= high)
{
return;
}
if ((high - low) == 1)
{
if (a[low] > a[high])
{
swap(a, low, high);
return;
}
}
int key = a[low];
int left = low + 1;
int right = high;
while (left < right)
{
while (left < right && left <= high)// 左边向右
{
if (a[left] >= key)
{
break;
}
left++;
}
while (right >= left && right > low)
{
if (a[right] <= key)
{
break;
}
right--;
}
if (left < right)
{
swap(a, left, right);
}
}
swap(a, low, right);
kuaisuSort(a, low, right);
kuaisuSort(a, right + 1, high);
}
四、插入排序
[java] view plain
package sort.insert;
/**
* 直接插入排序
* 将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据
* 算法适用于少量数据的排序,时间复杂度为O(n^2)。是稳定的排序方法。
*/
import java.util.Random;
public class DirectMain {
public static void main(String[] args) {
Random ran = new Random();
int[] sort = new int[10];
for (int i = 0; i < 10; i++) {
sort[i] = ran.nextInt(50);
}
System.out.print("排序前的数组为");
for (int i : sort) {
System.out.print(i + " ");
}
directInsertSort(sort);
System.out.println();
System.out.print("排序后的数组为");
for (int i : sort) {
System.out.print(i + " ");
}
}
/**
* 直接插入排序
*
* @param sort
*/
private static void directInsertSort(int[] sort) {
for (int i = 1; i < sort.length; i++) {
int index = i - 1;
int temp = sort[i];
while (index >= 0 && sort[index] > temp) {
sort[index + 1] = sort[index];
index--;
}
sort[index + 1] = temp;
}
}
}
顺便添加一份,差不多的
[java] view plain
public static void charuSort(int[] a)
{
int len = a.length;
for (int i = 1; i < len; i++)
{
int j;
int temp = a[i];
for (j = i; j > 0; j--)//遍历i之前的数字
{
//如果之前的数字大于后面的数字,则把大的值赋到后面
if (a[j - 1] > temp)
{
a[j] = a[j - 1];
} else
{
break;
}
}
a[j] = temp;
}
}
把上面整合起来的一份写法:
[java] view plain
/**
* 插入排序:
*
*/
public class InsertSort {
public void sort(int[] data) {
for (int i = 1; i < data.length; i++) {
for (int j = i; (j > 0) && (data[j] < data[j - 1]); j--) {
swap(data, j, j - 1);
}
}
}
private void swap(int[] data, int i, int j) {
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
五、顺便贴个二分搜索法
[java] view plain
package search.binary;
public class Main {
public static void main(String[] args) {
int[] sort = {1,2,3,4,5,6,7,8,9,10};
int mask = binarySearch(sort,6);
System.out.println(mask);
}
/**
* 二分搜索法,返回座标,不存在返回-1
* @param sort
* @return
*/
private static int binarySearch(int[] sort,int data){
if(data<sort[0] || data>sort[sort.length-1]){
return -1;
}
int begin = 0;
int end = sort.length;
int mid = (begin+end)/2;
while(begin <= end){
mid = (begin+end)/2;
if(data > sort[mid]){
begin = mid + 1;
}else if(data < sort[mid]){
end = mid - 1;
}else{
return mid;
}
}
return -1;
}
}
Ⅲ 面试中的计算题
算题者自己搞糊涂了,犯了逻辑上的错误。其实,根本就不存在少了10美元的问题。错误在于,说每个人实际交了90美元,共收3×90=270美元,这是对的,但不用这270美元去加20美元,而应加退给他们的30美元,正好等于300美元。因为人家交的270美元中,被服务员拿了20美元,这20美元就在270美元里面,怎么还能去和270美元相加呢?如果20美元要加,只能与经理那里的250美元相加,再加退给三人的30美元,总计300美元。这个问题实际上是个数学问题,也是个逻辑问题。
Ⅳ android 面试,算法题。
final int size = data.length;
for(int i = 0; i< size; i++){
if(data[i] == 0xffffffff)
data[i] = 0x80ffffff;
}
不知道你是不是这个意思。
Ⅳ java面试有哪些算法
面试-java算法题:
1.编写一个程序,输入n,求n!(用递归的方式实现)。
public static long fac(int n){ if(n<=0) return 0; else if(n==1) return 1; else return n*fac(n-1);
} public static void main(String [] args) {
System.out.println(fac(6));
}
2.编写一个程序,有1,2,3,4个数字,能组成多少个互不相同且无重复数字的三位数?都是多少?
public static void main(String [] args) { int i, j, k; int m=0; for(i=1;i<=4;i++) for(j=1;j<=4;j++) for(k=1;k<=4;k++){ if(i!=j&&k!=j&&i!=k){
System.out.println(""+i+j+k);
m++;
}
}
System.out.println("能组成:"+m+"个");
}
3.编写一个程序,将text1.txt文件中的单词与text2.txt文件中的单词交替合并到text3.txt文件中。text1.txt文件中的单词用回车符分隔,text2.txt文件中用回车或空格进行分隔。
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
public class text{
public static void main(String[] args) throws Exception{
String[] a = getArrayByFile("text1.txt",new char[]{'\n'});
String[] b = getArrayByFile("text2.txt",new char[]{'\n',' '});
FileWriter c = new FileWriter("text3.txt");
int aIndex=0; int bIndex=0;
while(aIndex<a.length){
c.write(a[aIndex++] + "\n");
if(bIndex<b.length)
c.write(b[bIndex++] + "\n");
}
while(bIndex<b.length){
c.write(b[bIndex++] + "\n");
}
c.close();
}
public static String[] getArrayByFile(String filename,char[] seperators) throws Exception{
File f = new File(filename);
FileReader reader = new FileReader(f);
char[] buf = new char[(int)f.length()];
int len = reader.read(buf);
String results = new String(buf,0,len);
String regex = null;
if(seperators.length >1 ){
regex = "" + seperators[0] + "|" + seperators[1];
}else{
regex = "" + seperators[0];
}
return results.split(regex);
}
}
4.639172每个位数上的数字都是不同的,且平方后所得数字的所有位数都不会出现组成它自身的数字。(639172*639172=408540845584),类似于639172这样的6位数还有几个?分别是什么?
这题采用的HashMap结构判断有无重复,也可以采用下题的数组判断。
public void selectNum(){
for(long n = 100000; n <= 999999;n++){
if(isSelfRepeat(n)) //有相同的数字,则跳过
continue;
else if(isPingFangRepeat(n*n,n)){ //该数的平方中是否有与该数相同的数字
continue;
} else{ //符合条件,则打印 System.out.println(n);
}
}
} public boolean isSelfRepeat(long n){
HashMap<Long,String> m=new HashMap<Long,String>(); //存储的时候判断有无重复值
while(n!=0){ if(m.containsKey(n%10)){ return true;
} else{
m.put(n%10,"1");
}
n=n/10;
} return false;
} public boolean isPingFangRepeat(long pingfang,long n){
HashMap<Long,String> m=new HashMap<Long,String>(); while(n!=0){
m.put(n%10,"1");
n=n/10;
} while(pingfang!=0){ if(m.containsKey(pingfang%10)){ return true;
}
pingfang=pingfang/10;
} return false;
} public static void main(String args[]){ new test().selectNum();
}
5.比如,968548+968545=321732732它的答案里没有前面两个数里的数字,有多少这样的6位数。
public void selectNum(){
for(int n = 10; n <= 99;n++){
for(int m = 10; m <= 99;m++){ if(isRepeat(n,m)){ continue;
} else{
System.out.println("组合是"+n+","+m);
}
}
}
} public boolean isRepeat(int n,int m){ int[] a={0,0,0,0,0,0,0,0,0,0}; int s=n+m; while(n!=0){
a[n%10]=1;
n=n/10;
} while(m!=0){
a[m%10]=1;
m=m/10;
} while(s!=0){ if(a[s%10]==1){ return true;
}
s=s/10;
} return false;
} public static void main(String args[]){ new test().selectNum();
}
6.给定String,求此字符串的单词数量。字符串不包括标点,大写字母。例如 String str="hello world hello hi";单词数量为3,分别是:hello world hi。
public static void main(String [] args) { int count = 0;
String str="hello world hello hi";
String newStr="";
HashMap<String,String> m=new HashMap<String,String>();
String [] a=str.split(" "); for (int i=0;i<a.length;i++){ if(!m.containsKey(a[i])){
m.put(a[i],"1");
count++;
newStr=newStr+" "+a[i];
}
}
System.out.println("这段短文单词的个数是:"+count+","+newStr);
}
7.写出程序运行结果。
public class Test1 { private static void test(int[]arr) { for (int i = 0; i < arr.length; i++) { try { if (arr[i] % 2 == 0) { throw new NullPointerException();
} else {
System.out.print(i);
}
} catch (Exception e) {
System.out.print("a ");
} finally {
System.out.print("b ");
}
}
}
public static void main(String[]args) { try {
test(new int[] {0, 1, 2, 3, 4, 5});
} catch (Exception e) {
System.out.print("c ");
}
}
}
运行结果:a b 1b a b 3b a b 5b
public class Test1 { private static void test(int[]arr) { for (int i = 0; i < arr.length; i++) { try { if (arr[i] % 2 == 0) { throw new NullPointerException();
} else {
System.out.print(i);
}
}
finally {
System.out.print("b ");
}
}
}
public static void main(String[]args) { try {
test(new int[] {0, 1, 2, 3, 4, 5});
} catch (Exception e) {
System.out.print("c ");
}
}
}
运行结果:b c
8.单词数
统计一篇文章里不同单词的总数。
Input
有多组数据,每组一行,每组就是一篇小文章。每篇小文章都是由小写字母和空格组成,没有标点符号,遇到#时表示输入结束。
Output
每组值输出一个整数,其单独成行,该整数代表一篇文章里不同单词的总数。
Sample Input
you are my friend
#
Sample Output
4
public static void main(String [] args) {
List<Integer> countList=new ArrayList<Integer>(); int count;
HashMap<String,String> m;
String str; //读取键盘输入的一行(以回车换行为结束输入) String[] a;
Scanner in=new Scanner(System.in);
while( !(str=in.nextLine()).equals("#") ){
a=str.split(" ");
m=new HashMap<String,String>();
count = 0; for (int i=0;i<a.length;i++){ if(!m.containsKey(a[i]) && (!a[i].equals(""))){
m.put(a[i],"1");
count++;
}
}
countList.add(count);
}s for(int c:countList)
System.out.println(c);
}
Ⅵ 大公司笔试面试有哪些经典算法题目
1、二维数组中的查找
具体例题:如果一个数字序列逆置之后跟原序列是一样的就称这样的数字序列为回文序列。例如:{1, 2, 1}, {15, 78, 78, 15} , {112} 是回文序列, {1, 2, 2}, {15, 78, 87, 51} ,{112, 2, 11} 不是回文序列。现在给出一个数字序列,允许使用一种转换操作:选择任意两个相邻的数,然后从序列移除这两个数,并用这两个数字的和插入到这两个数之前的位置(只插入一个和)。现在对于所给序列要求出最少需要多少次操作可以将其变成回文序列?
Ⅶ 一道经典的面试题:如何从N个数中选出最大(小)的n个数
这个问题我前前后后考虑了有快一年了,也和不少人讨论过。据我得到的消息,Google和微软都面过这道题。这道题可能很多人都听说过,或者知道答案(所谓的堆),不过我想把我的答案写出来。我的分析也许存有漏洞,以交流为目的。但这是一个满复杂的问题,蛮有趣的。看完本文,也许会启发你一些没有想过的解决方案(我一直认为堆也许不是最高效的算法)。在本文中,将会一直以寻找n个最大的数为分析例子,以便统一。注:本文写得会比较细节一些,以便于绝大多数人都能看懂,别嫌我罗嗦:) 我很不确定多少人有耐心看完本文! Naive 方法: 首先,我们假设n和N都是内存可容纳的,也就是说N个数可以一次load到内存里存放在数组里(如果非要存在链表估计又是另一个challenging的问题了)。从最简单的情况开始,如果n=1,那么没有任何疑惑,必须要进行N-1次的比较才能得到最大的那个数,直接遍历N个数就可以了。如果n=2呢?当然,可以直接遍历2遍N数组,第一遍得到最大数max1,但是在遍历第二遍求第二大数max2的时候,每次都要判断从N所取的元素的下标不等于max1的下标,这样会大大增加比较次数。对此有一个解决办法,可以以max1为分割点将N数组分成前后两部分,然后分别遍历这两部分得到两个最大数,然后二者取一得到max2。 也可以遍历一遍就解决此问题,首先维护两个元素max1,max2(max1=max2),取到N中的一个数以后,先和max1比,如果比max1大(则肯定比max2大),直接替换max1,否则再和max2比较确定是否替换max2。采用类似的方法,对于n=2,3,4一样可以处理。这样的算法时间复杂度为O(nN)。当n越来越大的时候(不可能超过N/2,否则可以变成是找N-n个最小的数的对偶问题),这个算法的效率会越来越差。但是在n比较小的时候(具体多小不好说),这个算法由于简单,不存在递归调用等系统损耗,实际效率应该很不错. 堆:当n较大的时候采用什么算法呢?首先我们分析上面的算法,当从N中取出一个新的数m的时候,它需要依次和max1,max2,max3max n比较,一直找到一个比m小的max x,就用m来替换max x,平均比较次数是n/2。可不可以用更少的比较次数来实现替换呢?最直观的方法是,也就是网上文章比较推崇的堆。堆有这么一些好处:1.它是一个完全二叉树,树的深度是相同节点的二叉树中最少的,维护效率较高;2.它可以通过数组来实现,而且父节点p与左右子节l,r点的数组下标的关系是s[l] = 2*s[p]+1和s[r] = 2*s[p]+2。在计算机中2*s[p]这样的运算可以用一个左移1位操作来实现,十分高效。再加上数组可以随机存取,效率也很高。3.堆的Extract操作,也就是将堆顶拿走并重新维护堆的时间复杂度是O(logn),这里n是堆的大小。 具体到我们的问题,如何具体实现呢?首先开辟一个大小为n的数组区A,从N中读入n个数填入到A中,然后将A维护成一个小顶堆(即堆顶A[0]中存放的是A中最小的数)。然后从N中取出下一个数,即第n+1个数m,将m与堆顶A[0]比较,如果m<=A[0],直接丢弃m。否则应该用m替换A[0]。但此时A的堆特性可能已被破坏,应该重新维护堆:从A[0]开始,将A[0]与左右子节点分别比较(特别注意,这里需要比较两次才能确定最大数,在后面我会根据这个来和败者树比较),如果A[0]比左右子节点都小,则堆特性能够保证,勿需继续,否则如左(右)节点最大,则将A[0]与左(右)节点交换,并继续维护左(右)子树。依次执行,直到遍历完N,堆中保留的n个数就是N中最大的n个数。 这都是堆排序的基本知识,唯一的trick就是维护一个小顶堆,而不是大顶堆。不明白的稍微想一下。维护一次堆的时间复杂度为O(logn),总体的复杂度是O(Nlogn)这样一来,比起上面的O(nN),当n足够大时,堆的效率肯定是要高一些的。当然,直接对N数组建堆,然后提取n次堆顶就能得到结果,而且其复杂度是O(nlogN),当n不是特别小的时候这样会快很多。但是对于online数据就没办法了,比如N不能一次load进内存,甚至是一个流,根本不知道N是多少。 败者树:有没有别的算法呢?我先来说一说败者树(loser tree)。也许有些人对loser tree不是很了解,其实它是一个比较经典的外部排序方法,也就是有x个已经排序好的文件,将其归并为一个有序序列。败者树的思想咋一看有些绕,其实是为了减小比较次数。首先简单介绍一下败者树:败者树的叶子节点是数据节点,然后两两分组(如果节点总数不是2的幂,可以用类似完全树的结构构成树),内部节点用来记录左右子树的优胜者中的败者(注意记录的是输的那一方),而优胜者则往上传递继续比较,一直到根节点。如果我们的优胜者是两个数中较小的数,则根节点记录的是最后一次比较中的败者,也就是所有叶子节点中第二小的那个数,而最小的那个数记录在一个独立的变量中。这里要注意,内部节点不但要记录败者的数值,还要记录对应的叶子节点。如果是用链表构成的树,则内部节点需要有指针指向叶子节点。这里可以有一个trick,就是内部节点只记录败者对应的叶子节点,具体的数值可以在需要的时候间接访问(这一方法在用数组来实现败者树时十分有用,后面我会讲到)。关键的来了,当把最小值输出后,最小值所对应的叶子节点需要变成一个新的数(或者改为无穷大,在文件归并的时候表示文件已读完)。接下来维护败者树,从更新的叶子节点网上,依次与内部节点比较,将败者更新,胜者往上继续比较。由于更新节点占用的是之前的最小值的叶子节点,它往上一直到根节点的路径与之前的最小值的路径是完全相同的。内部节点记录的败者虽然称为败者,但却是其所在子树中最小的数。也就是说,只要与败者比较得到的胜者,就是该子树中最小的那个数(这里讲得有点绕了,看不明白的还是找本书看吧,对照着图比较容易理解)。 注:也可以直接对N构建败者树,但是败者树用数组实现时不能像堆一样进行增量维护,当叶子节点的个数变动时需要完全重新构建整棵树。为了方便比较堆和败者树的性能,后面的分析都是对n个数构建的堆和败者树来分析的。 总而言之,败者树在进行维护的时候,比较次数是logn+1。与堆不同的是,败者树是从下往上维护,每上一层,只需要和败者节点比较一次即可。而堆在维护的时候是从上往下,每下一层,需要和左右子节点都比较,需要比较两次。从这个角度,败者树比堆更优一些。但是,请注意但是,败者树每一次维护必定需要从叶子节点一直走到根节点,不可能中间停止;而堆维护时,有可能会在中间的某个层停止,不需要继续往下。这样一来,虽然每一层败者树需要的比较次数比堆少一倍,但是走的层数堆会比败者树少。具体少多少,从平均意义上到底哪一个的效率会更好一些?那我就不知道了,这个分析起来有点麻烦。感兴趣的人可以尝试一下,讨论讨论。但是至少说明了,也许堆并非是最优的。 具体到我们的问题。类似的方法,先构建一棵有n个叶子节点的败者树,胜出者w是n个中最小的那一个。从N中读入一个新的数m后,和w比较,如果比w小,直接丢弃,否则用m替换w所在的叶子节点的值,然后维护该败者树。依次执行,直到遍历完N,败者树中保留的n个数就是N中最大的n个数。时间复杂度也是O(Nlogn) 类快速排序方法: 快速排序大家大家都不陌生了。主要思想是找一个轴节点,将数列交换变成两部分,一部分全都小于等于轴,另一部分全都大于等于轴,然后对两部分递归处理。其平均时间复杂度是O(NlogN)。从中可以受到启发,如果我们选择的轴使得交换完的较大那一部分的数的个数j正好是n,不也就完成了在N个数中寻找n个最大的数的任务吗?当然,轴也许不能选得这么恰好。可以这么分析,如果jn,则最大的n个数肯定在这j个数中,则问题变成在这j个数中找出n个最大的数;否则如果j<n,则这j个数肯定是n个最大的数的一部分,而剩下的j-n个数在小于等于轴的那一部分中,同样可递归处理。 需要注意的是,这里的时间复杂度是平均意义上的,在最坏情况下,每次分割都分割成1:N-2,这种情况下的时间复杂度为O(n)。但是我们还有杀手锏,可以有一个在最坏情况下时间复杂度为O(N)的算法,这个算法是在分割数列的时候保证会按照比较均匀的比例分割,at least 3n/10-6。具体细节我就不再说了,感兴趣的人参考算法导论(Introction to Algorithms 第二版第九章 Medians and Orders Statistics)。 还是那个结论,堆不见得会是最优的。 本文快要结束了,但是还有一个问题:如果N非常大,存放在磁盘上,不能一次装载进内存呢?怎么办?对于介绍的Naive方法,堆,败者树等等,依然适用,需要注意的就是每次从磁盘上尽量多读一些数到内存区,然后处理完之后再读入一批。减少IO次数,自然能够提高效率。而对于类快速排序方法,稍微要麻烦一些:分批读入,假设是M个数,然后从这M个数中选出n个最大的数缓存起来,直到所有的N个数都分批处理完之后,再将各批次缓存的n个数合并起来再进行一次类快速排序得到最终的n个最大的数就可以了。在运行过程中,如果缓存数太多,可以不断地将多个缓存合并,保留这些缓存中最大的n个数即可。由于类快速排序的时间复杂度是O(N),这样分批处理再合并的办法,依然有极大的可能会比堆和败者树更优。当然,在空间上会占用较多的内存。 总结:对于这个问题,我想了很多,但是觉得还有一些地方可以继续深挖:1. 堆和败者树到底哪一个更优?可以通过理论分析,也可以通过实验来比较。也许会有人觉得这个很无聊;2. 有没有近似的算法或者概率算法来解决这个问题?我对这方面实在不熟悉,如果有人有想法的话可以一块交流。如果有分析错误或遗漏的地方,请告知,我不怕丢人,呵呵!最后请时刻谨记,时间复杂度不等于实际的运行时间,一个常数因子很大的O(logN)算法也许会比常数因子小的O(N)算法慢很多。所以说,n和N的具体值,以及编程实现的质量,都会影响到实际效率。
Ⅷ java算法面试题
三个for循环,第一个和第二个有啥区别?去掉一个吧
可以用迭代器remove方法,在移除的同时添加。
不知道是你记错了还是题本身就这样,我只想说:
写这代码的是二货么?
1、每个循环的索引都是从0开始,这是什么遍历方式?
2、看这题的目的是想把用户添加到相应的组里,这我就不明白了,新建一个用户的时候就没分配组么?那用户的GroupId哪来的?
3、这是一个操作,难道就不会根据GroupId直接查出用户或者组么?
这哪是优化代码?分明是挖坑。