数据结构排序算法
选择排序
选择排序是一种简单的基于交换的排序。时间复杂度为O(N^2),算法的原理是每一次循环就确定一个坐标排好序的状态。
public class Main {
private static int aux[];//辅助数组
public static void main(String[] args) {
int a[] = {9, 8, 7, 6, 3};
aux = new int[a.length];
MergeSort(a);
System.out.println(Arrays.toString(a));
}
/**
* 交换
*
* @param a 数组
* @param preindex
* @param lastindex
*/
private static void Swap(int a[], int preindex, int lastindex) {
int temp = a[preindex];
a[preindex] = a[lastindex];
a[lastindex] = temp;
}
/**
* 选择排序
*
* @param a
*/
public static void SelectionSort(int[] a) {
int length = a.length;
for (int i = 0; i < length; i++) {
//外层循环找到是这个坐标对应的最小值。
int minIndex = i;
for (int j = i + 1; j < length; j++) {
if (a[j] < a[i]) minIndex = j;
}
Swap(a, minIndex, i);
}
}
}
算法特点:
- 运行时间与输入无关。也就是一个数组无论有序或者无序,在长度一定的情况下,他们所花费的时间是相同的。
- 数据移动是最少的。整个算法一共需要N次交换(尽管那个数据已经在所在的位置了,算法依然进行了一次交换)交换的次数与数组的长度是线性的关系,这是其他所有排序算法所不具备的。
插入排序
插入排序与打牌时整理牌的过程是一样的。我们拿到属于我们的全部牌后,整理时从左边开始,依次同前一张牌进行比较,如果较小就进行交换,如果前一张牌比这张牌小,那么就停止。因为前面的所有牌都比这张牌小了。
/**
* 插入排序
*
* @param a
*/
public static void InsertSort(int a[]) {
int length = a.length;
for (int i = 1; i < length; i++) {
//依次与上一个坐标的数字比较,如果小就互换,如果大就停止此次循环。
for (int j = i; j > 0 && a[j] < a[j - 1]; j--) {
Swap(a, j, j - 1);
}
}
}
算法特点:
- 从代码实现中就可以看出,“有序”对插入排序影响十分大(因为有停止条件)
- 当每个元素距离它的最终位置都不远时;
- 部分有序。
插入算法的改进
这是插入算法的另一种实现,《算法》一书中说可以节省时间,但是我还没有认真思考。这种升级的实现确实没有进行交换,但是频繁的数组移位应该也是一笔不小时间开销。PS:《算法》P168,2.1.25
/**
* 插入排序的升级版
* @param a
*/
public static void InsertUpdateSort(int a[]) {
int length = a.length;
for (int i = 1; i < length; i++) {
int temp = a[i];
int j;
for (j = i; j > 0 && temp < a[j - 1]; j--) {
a[j] = a[j - 1];
}
//说明temp比a[j-1]要大了,那么就是a[j]就是它这次循环的归宿。
//或者j=0了,触发了另一个终止循环的条件。那么a[j]也是它这次循环的归宿
a[j] = temp;
}
}
希尔排序
希尔排序是多次的插入排序,该算法的思想是每隔h个为一组,对这个组进行排序。排完之后再对下一个组进行排序。
如:
1,5,9;一组。
2,6,10;二组。
3,7,11;三组;
……
这都是坐标。
当每隔h组排完之后,在每隔h/2成一组再排序,一直到h=1变为了插入排序在最后依次排序。
Q:为什么不直接使用插入排序呢?
A:一般情况下,希尔排序的要比插入排序更快。
/**
* 希尔排序
*
* @param a
*/
public static void ShellsSort(int a[]) {
int length = a.length;
int h = length / 2;
while (h >= 1) {
//注意,代码最终的效果一样,并不是把一个组内所有的数据都拿出来然后归并排序。
for (int i = h; i < length; i++) {
for (int j = i; j >= h && a[j] < a[j - h]; j -= h) {
swap(a, j, j - h);
}
}
h /= 2;
}
}
归并排序
归并排序是一种基于分治策略的排序方法。它的主要方法是将数组一直对半分组,分到每一组中只有一个数字为止。需要注意的是这些分组并不是一个新的数组,而是使用坐标记录下这个虚拟分组的起止位置。这是数组的“排序”阶段。
接下来是归并。归并一定是两个相邻的数组才可以归并,而这两个数组之间通过一个坐标进行隔断。例如:low,mid,high。这里的mid就是隔断点。[low,mid]是一个数组,[mid+1,high]是另一个数组。
/**
* 归并排序
* @param a
*/
public static void MergeSort(int a[]) {
sort(a, 0, a.length - 1);
}
/**
* 分治
* @param a
* @param lo
* @param hi
*/
private static void sort(int a[], int lo, int hi) {
if (hi<=lo) return;
int mid = lo + (hi - lo) / 2;
sort(a, lo, mid);
sort(a, mid+1, hi);
merge(a, lo, mid, hi);
}
/**
* 归并操作
* @param a
* @param lo
* @param mid
* @param hi
*/
private static void merge(int a[], int lo, int mid, int hi) {
//在这里的操作,
int i=lo;
int j = mid + 1;
for (int k = lo; k <= hi; k++) {
aux[k] = a[k];
}
for (int k = lo; k <= hi; k++) {
if (i>mid) a[k] = aux[j++];
else if (j>hi) a[k] = aux[i++];
else if(aux[j]<aux[i]) a[k] = aux[j++];
else a[k] = aux[i++];
}
}
快速排序
快速排序是一种基于分治策略的排序,一般认为是最优秀的内部排序方法。
算法的基本思路是挑选一个点,设其为基点,建立两个指针,右指针然后从数组的右边开始向前依次查询,当碰到一个数小于基点时暂停;左指针从数组的左边开始向后一次查询,当碰到一个数大于基点时暂停,随后交换这两个数。依次进行,直到这两个指针相遇或者右指针跑到了左指针的左面。
当一次快速排序结束时,基点便位于它最终的位置,即它的左边都是不大于它的数,它的右边都是不小于它的数。随后以基点作为分界线,它的左边数组继续进行快速排序,右边数组也进行快速排序。这些都是在原来数组上以index来进行划分的。最终整个数组都是有序的 。
一般挑选数组最左边的数作为基点。
/**
* 快速排序
* @param a
* @param lo
* @param hi
*/
private static void QuickSort(int a[], int lo, int hi) {
if (lo >= hi) return;
int mid = partionsh(a, lo, hi);
QuickSort(a, lo, mid);
QuickSort(a, mid + 1, hi);
}
/**
* 筛选出基点
* @param a
* @param lo
* @param hi
* @return
*/
private static int partionsh(int[] a, int lo, int hi) {
int begin = a[lo];
int i = lo;
int j = hi;
while (true) {
//貌似必须从数组的右边先开始,如果和下面的代码交换顺序,会导致崩溃。
while (i < j && a[--j] > begin) {
if (j <= lo) break;
}
while (i < j && a[++i] < begin) {
if (i >= hi - 1) break;
}
if (i >= j) {
break;
}
Swap(a, i, j);
}
Swap(a, lo, i);
return i;
}