Java面试题
【排序算法】快速排序原理及Java实现
1、基本思想:
快速排序是我们之前学习的冒泡排序的升级,他们都属于交换类排序,都是采用不断的比较和移动来实现排序的。快速排序是一种非常高效的排序算法,它的实现,增大了记录的比较和移动的距离,将关键字较大的记录从前面直接移动到后面,关键字较小的记录从后面直接移动到前面,从而减少了总的比较次数和移动次数。同时采用“分而治之”的思想,把大的拆分为小的,小的拆分为更小的,其原理如下:对于给定的一组记录,选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分,直到序列中的所有记录均有序为止。
2、复杂度分析:
(1)最坏时间复杂度
最坏情况是指每次区间划分的结果都是基准关键字的左边(或右边)序列为空,而另一边区间中的记录仅比排序前少了一项,即选择的关键字是待排序记录的最小值或最大值。最坏情况下快速排序的时间复杂度为。
(2)最好时间复杂度
最好情况是指每次区间划分的结果都是基准关键字的左右两边长度相等或者相差为1,即选择的基准关键字为待排序的记录的中间值。此时进行比较次数总共为 nlogn,所以最好情况下快速排序的时间复杂度为。
(3)平均时间复杂度
快速排序的平均时间复杂度为。在所有平均时间复杂度为O(nlogn)的算法中,快速排序的平均性能是最好的。
(4)空间复杂度
快速排序的过程中需要一个栈空间来实现递归。最好情况,递归树的深度为,其空间复杂度也就是O(nlogn);最坏情况下,需要进行 n-1次递归,其空间复杂度为O(n);平均情况,空间复杂度为O(nlogn).
(5)基准关键字的选取,基准关键字的选取是决定快速排序算法的关键,常用的基准关键字的选取方式如下:
第一种:三者取中。将序列首、尾和中间位置上的记录进行比较,选择三者中值作为基准关键字。
第二种:取left和right之间的一个随机数,用n[m]作为基准关键字。采用这种方法得到的快速排序一般称为随机的快速排序。
3、排序过程如下:
以数组{49,38,65,97,76,13,27,49}为例,选择第一个元素49为基准
初始化关键字: [49,38,65,97,76,13,27,49]
4、Java实现如下:
public class QuickSort {
public static void sort(int a[], int low, int hight) {
int i, j, index;
if (low > hight) {
return;
}
i = low;
j = hight;
index = a[i]; // 用子表的第一个记录做基准
while (i < j) { // 从表的两端交替向中间扫描
while (i < j && a[j] >= index)
j--;
if (i < j)
a[i++] = a[j];// 用比基准小的记录替换低位记录
while (i < j && a[i] < index)
i++;
if (i < j) // 用比基准大的记录替换高位记录
a[j--] = a[i];
}
a[i] = index;// 将基准数值替换回 a[i]
sort(a, low, i - 1); // 对低子表进行递归排序
sort(a, i + 1, hight); // 对高子表进行递归排序
}
public static void quickSort(int a[]) {
sort(a, 0, a.length - 1);
}
public static void main(String[] args) {
int a[] = { 49, 38, 65, 97, 76, 13, 27, 49 };
quickSort(a);
System.out.println(Arrays.toString(a));
}
}
堆排序(Heapsort)之Java实现
堆排序算法介绍
堆是一种重要的数据结构,为一棵完全二叉树, 底层如果用数组存储数据的话,假设某个元素为序号为i(Java数组从0开始,i为0到n-1),如果它有左子树,那么左子树的位置是2i+1,如果有右子树,右子树的位置是2i+2,如果有父节点,父节点的位置是(n-1)/2取整。分为最大堆和最小堆,最大堆的任意子树根节点不小于任意子结点,最小堆的根节点不大于任意子结点。所谓堆排序就是利用堆这种数据结构来对数组排序,我们使用的是最大堆。处理的思想和冒泡排序,选择排序非常的类似,一层层封顶,只是最大元素的选取使用了最大堆。最大堆的最大元素一定在第0位置,构建好堆之后,交换0位置元素与顶即可。堆排序为原位排序(空间小), 且最坏运行时间是O(nlgn),是渐进最优的比较排序算法。
堆排序的思想是利用数据结构--堆。具体的实现细节:
1. 构建一个最大堆。对于给定的包含有n个元素的数组A[n],构建一个最大堆(最大堆的特性是,某个节点的值最多和其父节点的值一样大。这样,堆中的最大元 素存放在根节点中;并且,在以某一个节点为根的子树中,各节点的值都不大于该子树根节点的值)。从最底下的子树开始,调整这个堆结构,使其满足最大堆的特 性。当为了满足最大堆特性时,堆结构发生变化,此时递归调整对应的子树。
2. 堆排序算法,每次取出该最大堆的根节点(因为根节点是最大的),同时,取最末尾的叶子节点来作为根节点,从此根节点开始调整堆,使其满足最大堆的特性。
3. 重复上一步操作,直到堆的大小由n个元素降到2个。
4. gif 演示:http://upload.wikimedia.org/wikipedia/commons/4/4d/Heapsort-example.gif (来自wikipedia)
堆排序算法Java实现
如《插入排序(Insertsort)之Java实现》一样,先实现一个数组工具类。代码如下:
public static void printArray(int[] array) {
System.out.print("{");
for (int i = 0; i < array.length; i++) {
- System.outprint(array[i]);
- if (i < array.length - 1) {
- System.out.print(", ");
- }
- }
- System.out.println("}");
- }
- public static void exchangeElements(int[] array, int index1, int index2) {
- int temp = array[index1];
- array[index1] = array[index2];
- array[index2] = temp;
- }
- }
堆排序的大概步骤如下:
- 构建最大堆。
- 选择顶,并与第0位置元素交换
- 由于步骤2的的交换可能破环了最大堆的性质,第0不再是最大元素,需要调用maxHeap调整堆(沉降法),如果需要重复步骤2
堆排序中最重要的算法就是maxHeap,该函数假设一个元素的两个子节点都满足最大堆的性质(左右子树都是最大堆),只有跟元素可能违反最大堆性质,那么把该元素以及左右子节点的最大元素找出来,如果该元素已经最大,那么整棵树都是最大堆,程序退出,否则交换跟元素与最大元素的位置,继续调用maxHeap原最大元素所在的子树。该算法是分治法的典型应用。具体代码如下:
- public class HeapSort {
- public static void main(String[] args) {
- int[] array = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, -1, -2, -3 };
- System.out.println("Before heap:");
- ArrayUtils.printArray(array);
- heapSort(array);
- System.out.println("After heap sort:");
- ArrayUtils.printArray(array);
- }
- public static void heapSort(int[] array) {
- if (array == null || array.length <= 1) {
- return;
- }
- buildMaxHeap(array);
- for (int i = array.length - 1; i >= 1; i--) {
- ArrayUtils.exchangeElements(array, 0, i);
- maxHeap(array, i, 0);
- }
- }
- private static void buildMaxHeap(int[] array) {
- if (array == null || array.length <= 1) {
- return;
- }
- int half = array.length / 2;
- for (int i = half; i >= 0; i--) {
- maxHeap(array, array.length, i);
- }
- }
- private static void maxHeap(int[] array, int heapSize, int index) {
- int left = index * 2 + 1;
- int right = index * 2 + 2;
- int largest = index;
- if (left < heapSize && array[left] > array[index]) {
- largest = left;
- }
- if (right < heapSize && array[right] > array[largest]) {
- largest = right;
- }
- if (index != largest) {
- ArrayUtils.exchangeElements(array, index, largest);
- maxHeap(array, heapSize, largest);
- }
- }
- }
版权声明:本文为博主原创文章,未经博主允许不得转载。
- package service;
- import java.util.Scanner;
- public class Test003 {
- public static int Max = 20;
- // 数据数组源
- public static int data[] = { 12, 16, 19, 22, 25, 32, 39, 39, 48, 55, 57,58,63, 68, 69, 70, 78, 84, 88, 90, 97 };
- // 计数器
- public static int count = 1;
- public static void main(String[] args) {
- System.out.println("请输入您要查找的数字:");
- Scanner sc = new Scanner(System.in);
- int KeyValue = sc.nextInt();
- // 调用折半查找
- if (Search(KeyValue)) {
- // 输出查找次数
- System.out.println("共查找了" + count + "次");
- } else {
- // 输出没有找到数据
- System.out.println("抱歉,数据数组源中找不到您输入的数字");
- }
- }
- // 折半查找法
- public static boolean Search(int k) {
- int left = 0;// 左边界变量
- int right = Max - 1;// 右边界变量
- int middle;// 中位数变量
- while (left <= right) {
- middle = (left + right) / 2;
- if (k < data[middle]) {
- right = middle - 1;// 查找前半段
- } else if (k > data[middle]) {
- left = middle + 1;// 查找后半段
- } else if (k == data[middle]) {
- System.out.println("Data[" + middle + "] = " + data[middle]);
- return true;
- }
- count++;
- }
- return false;
- }
- }
-
Manacher算法及其Java实现
Manacher算法及其Java实现
原载于天意博文
说明
现给定一个已知的字符串str[],现在想要在O(n)的时间复杂度之内求出一个最长的回文子字符串(正着和倒着顺序读一致)。
Manacher最早发现了可以用O(n)的时间复杂度来解决该问题,所以这种方法称之为Manacher算法
实现步骤
基本过程
求最大回文字串的长度一般要看原串的长度是奇数还是偶数,然后再分别求得,但是Manacher算法的第一个神奇之处,就是把两种字符串都转化为奇数的字符串,从而简化计算:
// 1.构造新的字符串 // 为了避免奇数回文和偶数回文的不同处理问题,在原字符串中插入'#',将所有回文变成奇数回文 StringBuilder newStr = new StringBuilder(); newStr.append('#'); for (int i = 0; i < str.length(); i ++) { newStr.append(str.charAt(i)); newStr.append('#'); }
例如原来aaaba的字符串变化之后就是,而且无论原来的字符串是奇数还是偶数,变化之后都是奇数(方便运算)
当构建完成新的字符串之后,从左边第一个字符开始遍历,并且记录每一个字符的最大回文半径,(包括自身),比如第一个
#
的回文半径就是1,左边第一个A
的回文半径是2,遍历每一个字符之后,得到一个关于半径的数组,数组最大的值减1就是最大回文字串的长度,例如:完整实现
Manacher算法引入三个重要的概念(符号可能有所不同):
第一个是表示已知回文字串的中心点位置id,第二个是已知回文字串最右边的位置right,最后一个就是表示已知字串的回文半径数组rad[]
对应上面来说,id就是不断遍历的位置信息,right就是回文半径
+
id-
1,rad[]也就是把每一个半径存放起来确定最小半径
假设现在求出了rad[1, …, i],现在要求后面的rad值,再假设现在有个指针k(实际中就是1),从1循环到rad[i],试图通过某些手段来求出[i + 1, i + rad[i] - 1]的rad值
如图所示,黑色的部分是一个回文子串,两段红色的区间对称相等。因为之前已经求出了rad[i - k],所以可以避免一些重复的查找和判断,有3种情况:
- rad[i] - k < rad[i - k]
如图,rad[i - k]的范围为青色。因为黑色的部分是回文的,且青色的部分超过了黑色的部分,所以rad[i + k]肯定至少为rad[i]-k,即橙色的部分。那橙色以外的部分就不是了吗?这是肯定的,因为如果橙色以外的部分也是回文的,那么根据青色和红色部分的关系,可以证明黑色部分再往外延伸一点也是一个回文子串,这肯定是不可能的,因此rad[i + k] = rad[i] - k
- rad[i] - k > rad[i - k]
如图,rad[i-k]的范围为青色,因为黑色的部分是回文的,且青色的部分在黑色的部分里面,根据定义,很容易得出:rad[i + k] = rad[i - k]。
根据上面两种情况,可以得出结论:当rad[i] - k != rad[i - k]的时候,rad[i + k] = min(rad[i] - k, rad[i - k])
- rad[i] - k = rad[i - k]
如图,通过和第一种情况对比之后会发现,因为青色的部分没有超出黑色的部分,所以即使橙色的部分全等,也无法像第一种情况一样引出矛盾,因此橙色的部分是有可能全等的。
根据已知的信息,我们不知道橙色的部分是多长,因此就需要再去尝试和判断了,但是最少rad[i + k] = min(rad[i] - k, rad[i - k]),当然此时两者相等
具体代码
public static int getPalindromeLength(String str) { // 1.构造新的字符串 // 为了避免奇数回文和偶数回文的不同处理问题,在原字符串中插入'#',将所有回文变成奇数回文 StringBuilder newStr = new StringBuilder(); newStr.append('#'); for (int i = 0; i < str.length(); i ++) { newStr.append(str.charAt(i)); newStr.append('#'); } // rad[i]表示以i为中心的回文的最大半径,i至少为1,即该字符本身 int [] rad = new int[newStr.length()]; // right表示已知的回文中,最右的边界的坐标 int right = -1; // id表示已知的回文中,拥有最右边界的回文的中点坐标 int id = -1; // 2.计算所有的rad // 这个算法是O(n)的,因为right只会随着里层while的迭代而增长,不会减少。 for (int i = 0; i < newStr.length(); i ++) { // 2.1.确定一个最小的半径 int r = 1; if (i <= right) { r = Math.min(rad[id] - i + id, rad[2 * id - i]); } // 2.2.尝试更大的半径 while (i - r >= 0 && i + r < newStr.length() && newStr.charAt(i - r) == newStr.charAt(i + r)) { r++; } // 2.3.更新边界和回文中心坐标 if (i + r - 1> right) { right = i + r - 1; id = i; } rad[i] = r; } // 3.扫描一遍rad数组,找出最大的半径 int maxLength = 0; for (int r : rad) { if (r > maxLength) { maxLength = r; } } return maxLength - 1; }
复杂度分析
空间复杂度:插入分隔符形成新串,占用了线性的空间大小;RL数组也占用线性大小的空间,因此空间复杂度是线性的。
时间复杂度:尽管代码里面有两层循环,通过amortized analysis我们可以得出,Manacher的时间复杂度是线性的。由于内层的循环只对尚未匹配的部分进行,因此对于每一个字符而言,只会进行一次,因此时间复杂度是O(n)参考
http://blog.sina.com.cn/s/blog_3fe961ae0101iwc2.html
https://segmentfault.com/a/1190000003914228#articleHeader7
-