阿里面试常见问题

1、抽象类和普通类的区别:

抽象类不能被实例话,只能被继承,抽象方法可以没有实体,必需有子类来重写。由上面的可以看出抽象类就是为了作基类用的。可以定义子类的共同的方法,以方例多态使用。抽象类可以应用多态,但是普通类不可以。

抽象类只能作为基类,提供接口,不能有实例。普通类既可以作为基类,又可以有实例。

 

2、多态、重载和重写

override是重写(覆盖)了一个方法,以实现不同的功能。一般是用于子类在继承父类时,重写(重新实现)父类中的方法。

重写(覆盖)的规则:

   1、重写方法的参数列表必须完全与被重写的方法的相同,否则不能称其为重写而是重载.

   2、重写方法的访问修饰符一定要大于被重写方法的访问修饰符(public>protected>default>private)。

   3、重写的方法的返回值必须和被重写的方法的返回一致;

   4、重写的方法所抛出的异常必须和被重写方法的所抛出的异常一致,或者是其子类;

   5、被重写的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行重写。

   6、静态方法不能被重写为非静态的方法(会编译出错)。

overload是重载,一般是用于在一个类内实现若干重载的方法,这些方法的名称相同而参数形式不同。

 

重载的规则:

   1、在使用重载时只能通过相同的方法名、不同的参数形式实现。不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不一样);

   2、不能通过访问权限、返回类型、抛出的异常进行重载;

   3、方法的异常类型和数目不会对重载造成影响;

 

多态的概念比较复杂,有多种意义的多态,一个有趣但不严谨的说法是:继承是子类使用父类的方法,而多态则是父类使用子类的方法。

一般,我们使用多态是为了避免在父类里大量重载引起代码臃肿且难于维护。

 

3、多线程的实现方式

JAVA多线程实现方式主要有三种:继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的多线程。其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。

继承Thread类实现多线程
继承Thread类的方法尽管被我列为一种多线程实现方式,但Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。

  1. public class MyThread extends Thread {  
  2. public void run() {  
  3.  System.out.println("MyThread.run()");  
  4. }  
  5. }  

在合适的地方启动线程如下:

[java] view plaincopy

  1. MyThread myThread1 = new MyThread();  
  2. MyThread myThread2 = new MyThread();  
  3. myThread1.start();  
  4. myThread2.start();  


2、实现Runnable接口方式实现多线程
如果自己的类已经extends另一个类,就无法直接extends Thread,此时,必须实现一个Runnable接口,如下:

[java] view plaincopy

  1. public class MyThread extends OtherClass implements Runnable {  
  2. public void run() {  
  3.  System.out.println("MyThread.run()");  
  4. }  
  5. }  

为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:

[java] view plaincopy

  1. MyThread myThread = new MyThread();  
  2. Thread thread = new Thread(myThread);  
  3. thread.start();  

事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码:

[java] view plaincopy

  1. public void run() {  
  2. if (target != null) {  
  3.  target.run();  
  4. }  
  5. }  


3、使用ExecutorService、Callable、Future实现有返回结果的多线程
ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。想要详细了解Executor框架的可以访问http://www.javaeye.com/topic/366591 ,这里面对该框架做了很详细的解释。返回结果的线程是在JDK1.5中引入的新特征,确实很实用,有了这种特征我就不需要再为了得到返回值而大费周折了,而且即便实现了也可能漏洞百出。
可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。

 

4、线程池

线程池的作用:

线程池作用就是限制系统中执行线程的数量。
     根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

为什么要用线程池:

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService

 

5、链表的基本操作

#include<stdio.h>  
#include<stdlib.h>  
  
typedef struct node  
{  
    int data;  
    node* pNext;  
}Node;  
  
//链表的操作,以有头节点为例,无头节点类似  
Node* head = NULL;  
  
//创建链表,头结点data=0,pNext=NULL;  
bool createNodeList()  
{  
    head = (Node*) malloc(sizeof(Node));  
    if(NULL == head)  
    {  
        return false;  
    }  
    else  
    {  
        head->data = 0;  
        head->pNext = NULL;  
        return true;  
    }  
}  
  
//增加节点  
bool addNode(Node* node)  
{  
    if(NULL == head)  
    {  
        return false;  
    }  
    Node* p = head->pNext;  
    Node* q = head;  
    while(NULL != p)  
    {  
        q = p;  
        p = p->pNext;  
    }  
    q->pNext = node;  
    node->pNext = NULL;  
    return true;      
}  
  
//删除节点  
bool deleteNode(int index)  
{  
    if(NULL == head)  
    {  
        return false;  
    }  
    Node* p = head->pNext;  
      
    int length = 0;  
    while(NULL != p)  
    {  
        length ++;  
        p = p->pNext;  
    }  
  
    if(length < index)  
    {  
        return false;  
    }  
    else  
    {  
        Node* q = head;  
        p = head;  
        for(int i=0;i<index;i++)  
        {  
            q = p;  
            p = p->pNext;  
        }  
        Node* t = p->pNext;  
        q->pNext = t;  
        free(p);  
        return true;  
    }  
}  
  
//逆序  
void reverseNodeList()  
{  
    if(NULL == head)  
    {  
        return;  
    }  
    //如果链表长度为1  
    if(head->pNext == NULL)  
    {  
        return;  
    }  
    Node* p = head->pNext;  
    Node* q = p->pNext;  
    Node* t = NULL;  
    while(NULL != q)  
    {  
        t = q->pNext;  
        q->pNext = p;  
        p = q;  
        q = t;  
    }  
    head->pNext->pNext = NULL;  
    head->pNext = p;  
}  
  
//排序(降序)  
void sort()  
{  
    //冒泡排序  
    Node* pHead = head;  
    if(head == NULL)  
    {  
        return;  
    }  
    if(pHead->pNext == NULL)  
    {  
        return;  
    }  
    Node* pi = pHead->pNext;  
    Node* pj = pi->pNext;  
    for(;pi != NULL;pi=pi->pNext)  
    {  
        for(pj = pi->pNext;pj != NULL;pj=pj->pNext)  
        {  
            if(pj->data>pi->data)  
            {  
                int tmp = pj->data;  
                pj->data = pi->data;  
                pi->data = tmp;  
            }  
        }  
    }  
}  
//销毁  
void destroyNodeList()  
{  
    if(NULL == head)  
    {  
        return;  
    }  
    if(NULL == head->pNext)  
    {  
        free(head);  
        head = NULL;  
        return;  
    }  
    Node* p = head->pNext;  
    while(NULL != p)  
    {  
        Node* tmp = p;  
        p = p->pNext;  
        free(tmp);  
    }  
    free(head);  
    head = NULL;  
}  
  
void main()  
{  
    createNodeList();  
  
    Node* node1 = (Node*)malloc(sizeof(Node));  
    node1->data = 1;  
    node1->pNext = NULL;  
  
    Node* node2 = (Node*)malloc(sizeof(Node));  
    node2->data = 2;  
    node2->pNext = NULL;  
  
    addNode(node1);  
    addNode(node2);  
  
    reverseNodeList();  
  
    Node* node3 = (Node*)malloc(sizeof(Node));  
    node3->data = 3;  
    node3->pNext = NULL;  
  
    addNode(node3);  
  
    sort();  
  
    deleteNode(2);  
      
    destroyNodeList();  
} 

6、二叉树

定义:或为空树、或由根节点加上左子树和右子树组成。

满二叉树:每一层上节点数目都是最大节点数。

完全二叉树:树中1……n的节点标号和满二叉树中标号相同。具有n个节点的完全二叉树深度:[logn]+1(前面取下整)

遍历二叉树:根左右、左根右、左右根。

线索二叉树:

这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。线索链表解决了无法直接找到该结点在某种遍历序列中的前趋和后继结点的问题,出现了二叉链表找左、右孩子困难的问题。

7、排序算法

插入排序—直接插入排序(Straight Insertion Sort)

基本思想:将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。

如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

 选择排序

在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。

堆排序

由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)。若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。

初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。

因此,实现堆排序需解决两个问题:
1. 如何将n 个待排序的数建成堆;
2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。

冒泡排序

它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

快速排序

快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。

归并排序:

归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。归并排序示例:

8、搜索算法

深度优先搜索与广度优先搜索

有两种常用的方法可用来搜索图:即深度优先搜索和广度优先搜索。它们最终都会到达所有连通的顶点。深度优先搜索通过栈来实现,而广度优先搜索通过队列来实现。  

深度优先搜索:
下面图中的数字显示了深度优先搜索顶点被访问的顺序。

为了实现深度优先搜索,首先选择一个起始顶点并需要遵守三个规则:
(1) 如果可能,访问一个邻接的未访问顶点,标记它,并把它放入栈中。
(2) 当不能执行规则1时,如果栈不空,就从栈中弹出一个顶点。
(3) 如果不能执行规则1和规则2,就完成了整个搜索过程。

广度优先搜索:
在深度优先搜索中,算法表现得好像要尽快地远离起始点似的。相反,在广度优先搜索中,算法好像要尽可能地靠近起始点。它首先访问起始顶点的所有邻接点,然后再访问较远的区域。它是用队列来实现的。
下面图中的数字显示了广度优先搜索顶点被访问的顺序。

实现广度优先搜索,也要遵守三个规则:
(1) 访问下一个未来访问的邻接点,这个顶点必须是当前顶点的邻接点,标记它,并把它插入到队列中。
(2) 如果因为已经没有未访问顶点而不能执行规则1时,那么从队列头取一个顶点,并使其成为当前顶点。
(3) 如果因为队列为空而不能执行规则2,则搜索结束。

 

二分查找

  1. 二分查找的时间复杂度是O(log(n)),最坏情况下的时间复杂度是O(n)。
  2. 二分查找的一个条件是待查询的数组是有序的,我们假设这里的数组是升序的。
  3. 二分查找的主要思路就是设定两个指针start和end分别指向数组元素的收尾两端,然后比较数组中间结点arry[mid]和待查找元素。如果待查找元素小于中间元素,那么表明带查找元素在数组的前半段,那么将end=mid-1,如果待查找元素大于中间元素,那么表明该元素在数组的后半段,将start=mid+1;如果中间元素等于待查找元素,那么返回mid的值。

二分查找可以使用递归和非递归的方法来解决,下面给出代码实例。

  1. 6.  #include<iostream>
  2. 7.  #include<stdlib.h>
  3. 8.  using namespace std;
  4. 9.   
  5. 10. 

11.//不适用递归,如果存在返回数组位置,不存在则返回-1

12.int BinarySearch(int arry[],int len,int value)

13.{

  1. 14.    //如果传入的数组为空或者数组长度<=0那么就返回-1。防御性编程
  2. 15.    if(arry==NULL||len<=0)
  3. 16.        return -1;
  4. 17. 
  5. 18.    int start=0;
  6. 19.    int end=len-1;

   

  1. 20.    while(start<=end)//判断清是否有=
  2. 21.    {
  3. 22.        int mid=start+(end-start)/2;
  4. 23.        if(arry[mid]==value)
  5. 24.            return mid;
  6. 25.        else if(value<arry[mid])
  7. 26.            end=mid-1;
  8. 27.        else
  9. 28.            start=mid+1;
  10. 29.    }
  11. 30.    return -1;
  12. 31. 

32.}

 

原文地址:https://www.cnblogs.com/Vae1990Silence/p/4320852.html