面试遇到的问题及答案
一.struts1 & struts2 区别
Action 类:
Struts1要求Action类继承一个抽象基类。Struts1的一个普遍问题是使用抽象类编程而不是接口,而struts2的Action是接口。
Struts 2 Action类可以实现一个Action接口,也可实现其他接口,使可选和定制的服务成为可能。Struts2提供一个ActionSupport基类去 实现 常用的接口。Action接口不是必须的,任何有execute标识的POJO对象都可以用作Struts2的Action对象。
线程模式:
Struts1 Action是单例模式并且必须是线程安全的,因为仅有Action的一个实例来处理所有的请求。单例策略限制了Struts1 Action能作的事,并且要在开发时特别小心。Action资源必须是线程安全的或同步的。
Struts2 Action对象为每一个请求产生一个实例,因此没有线程安全问题。(实际上,servlet容器给每个请求产生许多可丢弃的对象,并且不会导致性能和垃圾回收问题)
Servlet 依赖:
Struts1 Action 依赖于Servlet API ,因为当一个Action被调用时HttpServletRequest 和 HttpServletResponse 被传递给execute方法。
Struts 2 Action不依赖于容器,允许Action脱离容器单独被测试。如果需要,Struts2 Action仍然可以访问初始的request和response。但是,其他的元素减少或者消除了直接访问HttpServetRequest 和 HttpServletResponse的必要性。
可测性:
测试Struts1 Action的一个主要问题是execute方法暴露了servlet API(这使得测试要依赖于容器)。一个第三方扩展--Struts TestCase--提供了一套Struts1的模拟对象(来进行测试)。
Struts 2 Action可以通过初始化、设置属性、调用方法来测试,“依赖注入”支持也使测试更容易。
捕获输入:
Struts1 使用ActionForm对象捕获输入。所有的ActionForm必须继承一个基类。因为其他JavaBean不能用作ActionForm,开发者经常创建多余的类捕获输入。动态Bean(DynaBeans)可以作为创建传统ActionForm的选择,但是,开发者可能是在重新描述(创建)已经存 在的JavaBean(仍然会导致有冗余的javabean)。
Struts 2直接使用Action属性作为输入属性,消除了对第二个输入对象的需求。输入属性可能是有自己(子)属性的rich对象类型。Action属性能够通过 web页面上的taglibs访问。Struts2也支持ActionForm模式。rich对象类型,包括业务对象,能够用作输入/输出对象。这种 ModelDriven 特性简化了taglib对POJO输入对象的引用。
表达式语言:
Struts1 整合了JSTL,因此使用JSTL EL。这种EL有基本对象图遍历,但是对集合和索引属性的支持很弱。
Struts2可以使用JSTL,但是也支持一个更强大和灵活的表达式语言--"Object Graph Notation Language" (OGNL).
绑定值到页面(view):
Struts 1使用标准JSP机制把对象绑定到页面中来访问。
Struts 2 使用 "ValueStack"技术,使taglib能够访问值而不需要把你的页面(view)和对象绑定起来。ValueStack策略允许通过一系列名称相同但类型不同的属性重用页面(view)。
类型转换:
Struts 1 ActionForm 属性通常都是String类型。Struts1使用Commons-Beanutils进行类型转换。每个类一个转换器,对每一个实例来说是不可配置的。
Struts2 使用OGNL进行类型转换。提供基本和常用对象的转换器。
校验:
Struts 1支持在ActionForm的validate方法中手动校验,或者通过Commons Validator的扩展来校验。同一个类可以有不同的校验内容,但不能校验子对象。
Struts2支持通过validate方法和XWork校验框架来进行校验。XWork校验框架使用为属性类类型定义的校验和内容校验,来支持chain校验子属性
Action执行的控制:
Struts1支持每一个模块有单独的Request Processors(生命周期),但是模块中的所有Action必须共享相同的生命周期。
Struts2支持通过拦截器堆栈(Interceptor Stacks)为每一个Action创建不同的生命周期。堆栈能够根据需要和不同的Action一起使用。
二.webservice工作原理
答案见我的博客
三.Hashmap的底层原理
答案见我的博客
四.集合
答案见我的博客
五.排序的方法,尤其快速排序
1.选择排序
定义:首先,选出数组中最小的元素,将它与数组中第一个元素交换。然后找出次小的元素,并将它与数组中第二个元素交换。按照这种方法一直进行下去,直到整个数组排完序。
交换次数:N-1
缺点:运行时间对文件已有序的部分依赖较少,从文件中选出最小元素的每一遍操作过程,并没有给出下一遍要找的最小元素的位置的相关消息。例如,该程序对已排好序的文件或各元素都相同的元素文件与对随机排列的文件排序所花的时间基本相同。
适用性:对于元素比较大,关键字又比较小的文件,应该选择该方法,而其他算法移动数据的步数都比选择排序更多。
sc(source code):
template <typename T, typename Compare>
void SelectSort(vector<T> & arr, Compare cp)
{
Display(arr, "before SelectSort:");
for (size_t i=0; i<arr.size(); ++i) {
size_t m = i;
for (size_t j=i+1; j<arr.size(); ++j) {
if ( !cp(arr[m], arr[j]) ) {
m = j;
}
}
if (m != i) {
swap(arr[i], arr[m]);
}
Display(arr, i);
}
}
2.插入排序
插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
算法描述:
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
-
从第一个元素开始,该元素可以认为已经被排序
-
取出下一个元素,在已经排序的元素序列中从后向前扫描
-
如果该元素(已排序)大于新元素,将该元素移到下一位置
-
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
-
将新元素插入到该位置中
-
重复步骤2
如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目。该算法可以认为是插入排序的一个变种,称为二分查找排序。
算法复杂度:
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数减去(n-1)次。平均来说插入排序算法复杂度为O(n2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。
sc:
template <typename T, typename Compare>
void InsertSort(vector<T>& arr, Compare cp)
{
Display(arr, "before InsertSort:");
for(size_t i=1; i<arr.size(); ++i) {
T temp = arr[i];
for(size_t j=i ; j>0 && !cp(temp, arr[j-1]) ; --j) {
arr[j] = arr[j-1];
arr[j-1] = temp;
Display(arr, "----");
}
Display(arr, i);
}
}
3.冒泡排序
定义:遍历文件,如果紧邻的两个元素大小顺序不对,就将两者交换,重复这样的操作直到整个文件排好序。
小结:
(1)选择排序使用大概次比较操作和N次交换操作。
(2)在平均情况下,插入排序执行大约次比较操作和次交换(移动)操作,而在最坏情况下需要两倍的数量。
(3)在平均情况和最坏情况下,冒泡排序执行大约次比较操作和次交换操作。
(4)对于逆序数[文件中一对顺序不对的元素称为一个逆序]是常数级的文件,插入排序和冒泡排序所要进行的比较和交换操作的数目的是输入的线性函数。
(5)如果文件中固有部分的元素的逆序数超过常数级,插入排序所执行的比较和交换操作仍是线性的。
(6)对于数据项较大,关键字较小的文件,选择排序的运行时间是线性的。
4.希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种高速而稳定的改进版本。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
-
插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率
-
但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位
算法实现
希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O(n2)的排序(冒泡排序或插入排序),可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。
一个更好理解的希尔排序实现:将数组列在一个表中并对列排序(用插入排序)。重复这过程,不过每次用更长的列来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身仅仅对原数组进行排序(通过增加索引的步长,例如是用i += step_size
而不是i++
)。
例如,假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5行的表中来更好地描述算法,这样他们就应该看起来是这样:
13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
然后我们对每列进行排序:
10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45
当我们以单行来读取数据时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].这时10已经移至正确位置了,然后再以3为步长进行排序:
10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45
排序之后变为:
10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94
最后以1步长进行排序(此时就是简单的插入排序了)。
步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。
Donald Shell 最初建议步长选择为n/2并且对步长取半直到步长达到1。虽然这样取可以比O(n2)类的算法(插入排序)更好,但这样仍然有减少平均时间和最差时间的余地。 可能希尔排序最重要的地方在于当用较小步长排序后,以前用的较大步长仍然是有序的。比如,如果一个数列以步长5进行了排序然后再以步长3进行排序,那么该数列不仅是以步长3有序,而且是以步长5有序。如果不是这样,那么算法在迭代过程中会打乱以前的顺序,那就不会以如此短的时间完成排序了。
已知的最好步长序列由Marcin Ciura设计(1,4,10,23,57,132,301,701,1750,…) 这项研究也表明“比较在希尔排序中是最主要的操作,而不是交换。”用这样步长序列的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。
另一个在大数组中表现优异的步长序列是(斐波那契数列除去0和1将剩余的数以黄金分割比的两倍的幂进行运算得到的数列):(1, 9, 34, 182, 836, 4025, 19001, 90358, 428481, 2034035, 9651787, 45806244, 217378076, 1031612713, …)
sc:
template <typename T, typename Compare>
void ShellSort(vector<T>& arr, Compare cp)
{
Display(arr, "before ShellSort");
int gap = 0;
while (gap <= arr.size()) {
gap = gap * 3 + 1;
}
T temp;
while (gap > 0) {
for (int i=gap; i<arr.size(); ++i) {
int j = i - gap;
temp = arr[i];
while (( j>=0 ) && ( cp(arr[j], temp) )) {
arr[j+gap] = arr[j];
j -= gap;
}
arr[j + gap] = temp;
}
gap = (gap - 1) / 3;
Display(arr, gap);
}
Display(arr, "end sort");
}
5.快速排序
我之前的一篇随笔:快速排序
快速排序算法是一种分治排序算法。它将数组划分为两个部分,然后分别对两个部分进行排序。划分的准确位置取决于输入文件中元素的初始位置。关键在于划分过程,它重排元素,使得以下三个条件成立:
对于某一i,a[i]在数组的最终位置上;
a[1],…,a[i-1]中的元素都比a[i]小;
a[i+1],…,a[r]中的元素都比a[i]大。
我们通过划分来完成排序,然后递归地应用该方法处理子文件。
递归实现:
sc:
int Partition(vector<int> & arr, int left, int right)
{
int i = left - 1;
int j = right;
int v = arr[right];
for (;;) {
while (arr[++i] < v) ;
while (arr[--j] > v) {
if (j == left) {
break;
}
}
if (i >= j) {
break;
}
swap(arr[i], arr[j]);
}
swap(arr[i], arr[right]);
return i;
}
void QuickSort(vector<int> & arr, int left, int right)
{
if (right <= left) return ;
int i = Partition(arr, left, right);
QuickSort(arr, left, i-1);
QuickSort(arr, i+1, right);
}
非递归实现:
sc:
int Partition(vector<int> & arr, int left, int right)
{
int i = left - 1;
int j = right;
int v = arr[right];
for (;;) {
while (arr[++i] < v) ;
while (arr[--j] > v) {
if (j == left) {
break;
}
}
if (i >= j) {
break;
}
swap(arr[i], arr[j]);
}
swap(arr[i], arr[right]);
return i;
}
void QuickSort(vector<int> & arr, int left, int right)
{
stack< pair<int, int> > sk;
sk.push(make_pair<int, int>(left, right));
while (!sk.empty()) {
pair<int, int> lr = sk.top();
sk.pop();
if (lr.first >= lr.second) continue;
int i = Partition(arr, lr.first, lr.second);
if (i-lr.first > lr.second-i) {
sk.push(make_pair<int, int>(lr.first, i-1));
sk.push(make_pair<int, int>(i+1, lr.second));
}
else {
sk.push(make_pair<int, int>(i+1, lr.second));
sk.push(make_pair<int, int>(lr.first, i-1));
}
}
}
快速排序的性能特征:
(1)快速排序最坏情况下使用大约次比较。
(2)快速排序平均情况下使用大约2NlgN次比较。
(3)如果两个子文件的较小者首先是排好序的,那么在使用快速排序对N个元素进行排序时,栈中元素不会超过lgN个。
6.归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
- //将有序数组a[]和b[]合并到c[]中
- void MemeryArray(int a[], int n, int b[], int m, int c[])
- {
- int i, j, k;
- i = j = k = 0;
- while (i < n && j < m)
- {
- if (a[i] < b[j])
- c[k++] = a[i++];
- else
- c[k++] = b[j++];
- }
- while (i < n)
- c[k++] = a[i++];
- while (j < m)
- c[k++] = b[j++];
- }
可以看出合并有序数列的效率是比较高的,可以达到O(n)。
解决了上面的合并有序数列问题,再来看归并排序,其的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?
可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。
- //将有二个有序数列a[first...mid]和a[mid...last]合并。
- void mergearray(int a[], int first, int mid, int last, int temp[])
- {
- int i = first, j = mid + 1;
- int m = mid, n = last;
- int k = 0;
- while (i <= m && j <= n)
- {
- if (a[i] <= a[j])
- temp[k++] = a[i++];
- else
- temp[k++] = a[j++];
- }
- while (i <= m)
- temp[k++] = a[i++];
- while (j <= n)
- temp[k++] = a[j++];
- for (i = 0; i < k; i++)
- a[first + i] = temp[i];
- }
- void mergesort(int a[], int first, int last, int temp[])
- {
- if (first < last)
- {
- int mid = (first + last) / 2;
- mergesort(a, first, mid, temp); //左边有序
- mergesort(a, mid + 1, last, temp); //右边有序
- mergearray(a, first, mid, last, temp); //再将二个有序数列合并
- }
- }
- bool MergeSort(int a[], int n)
- {
- int *p = new int[n];
- if (p == NULL)
- return false;
- mergesort(a, 0, n - 1, p);
- delete[] p;
- return true;
- }
归并排序的效率是比较高的,设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。因为归并排序每次都是在相邻的数据中进行操作,所以归并排序在O(N*logN)的几种排序方法(快速排序,归并排序,希尔排序,堆排序)也是效率比较高的。
7.堆排序
堆是一种重要的数据结构,为一棵完全二叉树, 底层如果用数组存储数据的话,假设某个元素为序号为i(Java数组从0开始,i为0到n-1),如果它有左子树,那么左子树的位置是2i+1,如果有右子树,右子树的位置是2i+2,如果有父节点,父节点的位置是(n-1)/2取整。分为最大堆和最小堆,最大堆的任意子树根节点不小于任意子结点,最小堆的根节点不大于任意子结点。所谓堆排序就是利用堆这种数据结构来对数组排序,我们使用的是最大堆。处理的思想和冒泡排序,选择排序非常的类似,一层层封顶,只是最大元素的选取使用了最大堆。最大堆的最大元素一定在第0位置,构建好堆之后,交换0位置元素与顶即可。堆排序为原位排序(空间小), 且最坏运行时间是O(nlgn),是渐进最优的比较排序算法。
8.基数排序
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
附上述代码中缺失Display方法代码:
template <typename T>
void Display(const vector<T> & arr, size_t count)
{
cout << count << ": ";
copy (arr.begin(), arr.end(), ostream_iterator<T>(cout, " "));
cout << endl;
}
template <typename T>
void Display(const vector<T> & arr, string prompt=string())
{
cout << prompt << " ";
copy (arr.begin(), arr.end(), ostream_iterator<T>(cout, " "));
cout << endl;
}
6.struts2的反射
7.hibernate与mybatis区别及工作原理
8.SpringMVC的工作流程
9.SpringMVC和struts2是单例还是多例模式?
10.Oracle和Mysql的分页技术
11.设计模式,尤其单例模式的分类和用途
12.pui实现excel表格和word文档的工作原理
13.内置对象