文章目录
概述
注:此博文均以将数组进行从小到大排序示例,且给定待排序数组从索引0开始。
main方法代码:
import java.util.Arrays;
import java.util.Collections;
import java.util.Scanner;
public class SortAlgorithm {
public static void main(String[] args) {
int n;
Scanner sc = new Scanner(System.in);
while(sc.hasNext()) {
n = sc.nextInt();
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = sc.nextInt();
}
//insertSort1(a);
//insertSort2(a);
//binInsertSort(a);
//shellSort(a);
//selctionSort(a);
//bubbleSort1(a);
//bubbleSort2(a);
//quickSort(a, 0, n - 1);
//quickSort2(a, 0, n - 1);
//heapSort(a);
mergeSort(a, 0, n - 1, new int[n]);
for (int i = 0; i < n; i++) {
System.out.print(a[i] + " ");
}
System.out.println();
}
}
public static void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
插入排序
- 直接插入排序
1. 思路:
每次将一个待排序值插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止。
2. 代码: 两种写法
/**
* 直接插入排序: 写法1(元素右移)
*
* 每次将当前待排序值先赋值给临时变量,空出这个位置,该值与其前面已有序的值从后往前不断比较,将较大的元素不断右移,直到该待排序值找到合适位置并插入。
* 从数组第二个值开始,循环此过程直至最后一个待排序值完成插入。
*/
public static void insertSort1(int[] a) {
for (int j, i = 1; i < a.length; i++) {
if (a[i] < a[i-1]) {
int temp = a[i];
for (j = i - 1; j >= 0 && temp < a[j]; j--) {
a[j + 1] = a[j];
}
a[j + 1] = temp;
}
}
}
/**
* 直接插入排序: 写法2(元素交换)
*
* 每次将当前待排序值与其前面已排好序的值从后往前不断进行比较和交换,直到将该待排序值插入到合适位置。
* 从数组第二个值开始,循环此过程直至最后一个待排序值完成插入。
*/
public static void insertSort2(int[] a) {
for (int i = 1; i < a.length; i++) {
for (int j = i - 1; j >= 0 && a[j] > a[j + 1]; j--) {
swap(a, j, j + 1);
}
}
}
3. 总结:
插入排序所需的时间取决于待排序数组元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比随机顺序的数组或是逆序数组进行排序要快得多。
- 二分插入排序(折半插入排序)
1. 思路:
在上述直接插入排序写法1的基础上,通过二分查找来找到插入位置。
2. 代码:
/**
* 二分插入排序(折半插入排序):
*
* 在直接插入排序写法1的基础上,先通过二分查找找到待排序值在其前面已有序数组中合适的插入位置,再不断进行元素右移,最后插入。
* 从数组第二个值开始,循环此过程直至最后一个待排序值完成插入。
*/
public static void binInsertSort(int a[]) {
for (int j, i = 1; i < a.length; i++) {
if (a[i] < a[i-1]) {
int temp = a[i];
int left = 0, right = i - 1, mid;
while (left <= right) {
mid = (left + right) >> 1;
if (temp < a[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
for (j = i - 1; j >= right + 1; j--) {
a[j + 1] = a[j];
}
a[right + 1] = temp;
}
}
}
- 希尔排序(递减增量排序)
1. 思路:
将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序;每次再将gap折半减小,循环上述操作;当gap=1时,利用直接插入,完成排序。
2. 代码:
/**
* 希尔排序(递减增量排序):
*/
public static void shellSort(int[] a) {
int gap = a.length;
while (true) {
if (gap == 1) {
break;
}
gap /= 2; //增量每次减半
for (int k = 0; k < gap; k++) { //根据增量分为若干子序列
for (int i = k + gap; i < a.length; i += gap) { //这个循环里其实就是一个直接插入排序
for (int j = i; j > k && a[j] < a[j - gap]; j -= gap) {
swap(a, j, j - gap);
}
}
}
}
}
3. 总结:
希尔排序中对于增量序列的选择十分重要,直接影响到希尔排序的性能。我们上面选择的增量序列{n/2,(n/2)/2…1}(希尔增量),其最坏时间复杂度依然为O(n2),一些经过优化的增量序列如Hibbard经过复杂证明可使得最坏时间复杂度为O(n3/2)。
选择排序
- 简单选择排序
1. 思路:
每次将一个待排序数与其后面的未排序数逐个比较,从其后面的子序列中选择出最小的值与当前待排序数交换。
2. 代码:
/**
* 简单选择排序:
*
* 在长度为n的无序数组中,
* 第1次遍历从第2个数开始的n-1个数,找到最小的数值与第一个元素交换;
* 第2次遍历从第3个数开始的n-2个数,找到最小的数值与第二个元素交换;
* ...
* 第n-1次遍历从第n个数开始的最后1个数,找到最小的数值与第n-1个元素交换,排序完成。
*/
public static void selctionSort(int[] a) {
for (int i = 0; i < a.length - 1; i++) {
int temp = i;
for (int j = i + 1; j < a.length; j++) {
if (a[j] < a[temp]) {
temp = j;
}
}
if (temp != i) {
swap(a, i, temp);
}
}
}
- 堆排序
1. 基本理论:
堆的含义:完全二叉树中任何一个非叶子节点的值均不大于(或不小于)其左,右孩子节点的值。堆是一种逻辑结构,把堆的数据按层进行编号,然后存储在相应的数组里。
大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。因此我们可使用大顶堆进行从小到大排序,使用小顶堆进行从大到小排序。
2. 思路:
1.建堆:先将一个无序数组建成完全二叉树,然后从最后一个非叶子结点开始,从右至左,从下至上进行调整,将他调整成大顶堆。
【最后一个非叶子结点的下标: (arry.length-1-1)/2 = array.length/2-1
】
2.此时,调整成大顶堆后整个序列的最大值就是堆顶的根节点
3.将其与末尾元素进行交换,此时末尾就为最大值
4.然后将剩余 n-1 个元素重新构造成一个大顶堆,再进行2、3过程,这样就会得到原数组的次大值。如此循环此过程,便能得到一个有序序列了。
3. 代码:
/**
* 堆排序
* @param array 待排序数组
*/
public static void heapSort(int[] array) {
//这里元素的索引是从0开始的,所以最后一个非叶子结点array.length/2 - 1
for (int i = array.length / 2 - 1; i >= 0; i--) {
adjustHeap(array, i, array.length); //调整堆
}
// 上述逻辑,建堆结束
// 下面,开始排序逻辑
for (int j = array.length - 1; j > 0; j--) {
// 元素交换,作用是去掉大顶堆
// 把大顶堆的根元素,放到数组的最后;换句话说,就是每一次的堆调整之后,都会有一个元素到达自己的最终位置
swap(array, 0, j);
// 元素交换之后,毫无疑问,最后一个元素无需再考虑排序问题了。
// 接下来我们需要排序的,就是已经去掉了部分元素的堆了,这也是为什么此方法放在循环里的原因
// 而这里,实质上是自上而下,自左向右进行调整的
adjustHeap(array, 0, j); //写法1:循环判断
// adjustHeap2(array, 0, j); //写法2:递归调用
}
}
/**
* 调整堆:写法1(循环判断)
* @param array 待组堆
* @param index 起始结点
* @param length 堆的长度
*/
public static void adjustHeap(int[] array, int index, int length) {
// 先把当前元素取出来,因为当前元素可能要一直移动
int tempMax = array[index];
for (int k = 2 * index + 1; k < length; k = 2 * k + 1) { //2*i+1为i的左子树(因为i是从0开始的),2*k+1为k的左子树
// 让k先指向子节点中最大的节点
if (k + 1 < length && array[k] < array[k + 1]) { //如果有右子树,并且右子树大于左子树
k++;
}
//如果发现结点(左右子结点)大于根结点,则进行值的交换
if (array[k] > tempMax) {
swap(array, index, k);
// 如果子节点更换了,那么,以子节点为根的子树会受到影响,所以,循环对子节点所在的树继续进行判断
index = k;
} else { //不用交换,直接终止循环
break;
}
}
}
/**
* 调整堆:写法2(递归写法)
* @param array 待组堆
* @param index 起始结点
* @param length 堆的长度
*/
public static void adjustHeap2(int[] array, int index, int length) {
int max = index;
if (2 * index + 1 < length && array[2 * index + 1] > array[max]) {
max = 2 * index + 1; //max记为左结点
}
if (2 * index + 2 < length && array[2 * index + 2] > array[max]) {
max = 2 * index + 2; //max记为右结点
}
if (max != index) {
swap(array, max, index); //交换
adjustHeap2(array, max, length); //递归对调用的子结点进行调整
}
}
4. 总结:
由于每次重新恢复堆的时间复杂度为O(logN),共N - 1次重新恢复堆操作,再加上前面建立堆时N / 2次向下调整,每次调整时间复杂度也为O(logN)。二次操作时间相加还是O(N * logN)。堆排序适合于数据量非常大的场合(百万数据)。
题目:设有n个无序的元素,希望用最快的速度挑选出其中前50个最大的元素,请选择最好的排序法(C)
A、冒泡排序
B、基数排序
C、堆排序
D、快速排序
解析:堆排序算法中,数组最大的元素位于堆顶处,在输出堆项的最大值之后,使得剩余n-1个元素的序列又建成一个堆,则得到n个元素中的次大值。如此反复执行50次,便能得到前50个最大的元素。和其他的相比堆排序只执行了50次,其他的在最坏的情况下都要遍历,所以C。
所以堆排序适用于那些如上述题目的部分排序的情况,可见一道部分排序题
交换排序
- 冒泡排序
1. 思路:
它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
2. 代码:
/**
* 冒泡排序:
*
* 循环n-1趟以下过程:
* 每一趟不断比较相邻的两个数,将较大的数交换位置放到后面
*
*/
public static void bubbleSort1(int a[]) {
for (int i = 0; i < a.length - 1; i++) {
for (int j = 0; j < a.length - i - 1; j++) { //也可从后往前遍历 for(int j = a.length-1; j > i; j--)
if (a[j] > a[j + 1]) {
swap(a, j, j + 1);
}
}
}
}
两个优化
1.数据的顺序排好之后,冒泡算法仍然会继续进行下一轮的比较,直到arr.length-1次,后面的比较没有意义的。
2.记录每一趟的最后一次交换位置,下一趟排序只需遍历到该位置,因为后面没发生交换的序列已经是有序的了。
/**
* 冒泡排序:优化
*/
public static void bubbleSort2(int a[]) {
int book;
int sortOrder = a.length - 1;
for (int i = 0; i < a.length - 1; i++) { //确定排序趟数
book=0; //作为标记,同时也用来记录最后一次交换的位置
for (int j = 0; j < sortOrder; j++) { //确定比较次数
if (a[j] > a[j + 1]) { //a[j]和a[j+1]交换
swap(a, j, j + 1);
book = j;
}
}
if (book == 0) {
break;
}
sortOrder = book;
}
}
- 快速排序
1.思路:
基本思想:分治
1.先从数列中取出一个数作为基准数;
2.将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
3.对左右两个小数列重复第二步,直至各区间只有1个数。
基准数的选取可以有多种形式,例如中间数或者随机数,分别会对算法的复杂度产生不同的影响。
2. 代码: 两种写法
/**
* 快速排序:写法1 挖坑法
*/
public static void quickSort(int a[], int left, int right) {
if (left >= right) {
return;
}
int pivotkey = a[left]; //每次以数组的第一个数为基准数
int i = left, j = right;
while(i < j) {
//先向右后向左
//从右往左找到比基准数小的,把该值赋给a[i]
while (i < j && pivotkey <= a[j]) {
j--;
}
a[i] = a[j];
//从左往右找到比基准数大的,把该值赋给a[j]
while (i < j && pivotkey >= a[i]) {
i++;
}
a[j] = a[i];
}
//找到基准数的正确位置i(最后结果是其左边的均小于基准数,其右边的均大于基准数)
a[i] = pivotkey;
quickSort(a, left, i - 1);
quickSort(a, i + 1, right);
}
/**
* 快速排序:写法2 交换法
*
*/
public static void quickSort2(int a[], int left, int right) {
if (left >= right) {
return;
}
int pivotkey = a[left];//每次以数组第一个数为基准数
int i = left, j = right;
while(i < j) {
//先向右后向左
//从右往左找到比基准数小的
while (i < j && pivotkey <= a[j]) {
j--;
}
//从左往右找到比基准数大的
while (i < j && pivotkey >= a[i]) {
i++;
}
//两个数交换
if (i < j) {
swap(a, i, j);
}
}
//找到基准数的正确位置i,与基准数的原位置left进行两个值交换(交换完成基准数左边均比他小,右边均比他大)
a[left] = a[i];
a[i] = pivotkey;
quickSort(a, left, i - 1);
quickSort(a, i + 1, right);
}
3. 总结:
快速排序比大部分排序算法都要快。尽管我们可以在某些特殊的情况下写出比快速排序快的算法,但是就通常情况而言,没有比它更快的了。但快速排序是递归的,对于内存非常有限的机器来说,它不是一个好的选择。
归并排序
1. 思路:
基本思想:分治
2. 代码:
/**
* 归并排序
* @param a 原无序数组
* @param first 0
* @param last a.length-1
* @param temp 临时数组
*/
public static 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); //再将二个有序数列合并
}
}
/**
* 合并两个有序子序列
*/
private static void mergeArray(int[] a, int first, int mid, int last, int[] temp) {
int l1 = first, r1 = mid;
int l2 = mid + 1, r2 = last;
int k = 0;
//将左右两个有序子数组进行合并
while (l1 <= r1 && l2 <= r2) {
if (a[l1] <= a[l2]) {
temp[k++] = a[l1++];
} else {
temp[k++] = a[l2++];
}
}
while (l1 <= r1) {
temp[k++] = a[l1++];
}
while (l2 <= r2) {
temp[k++] = a[l2++];
}
//将temp得到的有序数组复制到原数组a中
for (int i = 0; i< k; i++) {
a[first + i] = temp[i];
}
}
3.总结:
- 速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。
- 归并排序的比较次数小于快速排序的比较次数,移动次数一般多于快速排序的移动次数。
- 归并排序的效率是比较高的,设数列长为 N,将数列分开成小数列一共要 logN 步,每步都是一个合并有序数列的过程,时间复杂度可以记为 O(N),故时间复杂度一共为 O(N*logN)。
- 归并排序需要用到一个临时数组temp,故空间复杂度为O(N)。