标签:
/**
* 插入排序
* @param data
*/
public static void insertSort(int[] data){
for (int i = 1; i < data.length; ++i){
int curData = data[i];
int j = i - 1;
while (j >= 0){
if (curData < data[j]){
data[j + 1] = data[j];
j--;
}else{
break;
}
}
data[j + 1] = curData;
}
}
/**
* 在一定范围内进行插入排序, 快速排序时使用
* @param data
* @param left
* @param right
*/
public static void insertSortWithBound(int[] data, int left, int right){
for (int i = left + 1; i <= right; ++i){
int curData = data[i];
int j = i - 1;
while (j >= 0){
if (curData < data[j]){
data[j + 1] = data[j];
j--;
}else{
break;
}
}
data[j + 1] = curData;
}
}
/*********************************************************************************************************************************/
// 插入排序声明, 自己实现的, 不够简洁
// 插入排序有一个特点: 那就是已经排好序的元素就是最终的元素, 利用插入排序也可以实现查找第k小的元素. 只是相对于运用优先队列来说耗时较长
// 插入排序的平均和最坏时间复杂度都是O(N^2)
void InsertSort(int data[], int length)
{
int temp, i = 0, j = 0;
for (i = 1; i < length; ++i)
{
temp = data[i];
while(j >= 0)
{
if(temp < data[j]){
data[j + 1] = data[j]; // 如果data[j+1]位置的元素比data[i]中的元素大的话, 就往后移动一个位置.
--j;
}
else
break; // 如果data[j+1]位置的元素比data[i]中的元素小的话, 说明后面的那个空位就是data[i]的正确位置
}
data[j + 1] = temp; // 将data[i]放到正确的位置到
}
}
/*********************************************************************************************************************************/
/**
* 选择排序
* @param data
*/
public static void selectSort(int[] data){
for (int i = 0; i < data.length - 2; ++i){
int temp = i;
for (int j = i + 1; j < data.length - 1; ++j){
if (data[temp] > data[j])
temp = j;
}
swap(data, i, temp);
}
}
/**
* 冒泡排序
*/
public static void bubbleSort(int[] data){
for (int i = data.length - 1; i > 0; --i){
int flag = 0;
for (int j = 0; j < i; ++j){
if (data[j] > data[j + 1]){
swap(data, j, j + 1);
++flag;
}
}
// 如果在一次冒泡中一次都没有进行交换, 那么这个数组已经是有序了,这个操作会提高冒泡排序的效率
if (flag == 0)
break;
}
}
注意:希尔排序的效率依赖于增量序列的选择, 所以对于选择使用不同的增量序列进行希尔排序,效率会相差很大;通过前人的实践得出了两个效率高的两个增量序列1. 选择一个步长序列t1, t2, ....., tk, 满足ti > tj, tk = 12. 按步长序列个数k, 对待排序列进行k趟排序3. 每趟排序,根据对应的步长ti,将待排序列分割成ti个子序列, 分别对各个子序列进行插入排序4. 最后一趟的步长为1, 也就是说直接对整个数组进行了一次直接插入排序,因为经过前面的步骤后,数组中的元素已经基本有序,此时再进行一次插入排序效率会很高。
希尔排序图示:1. Hibbard增量序列: 1, 3, 7, ....., 2^k - 1; 采用此增量序列的希尔排序的最坏运行时间是N^(5/4).2. sedgewick增量序列: 4^i - 3*2^i + 1; 采用此增量序列的希尔排序的最坏运行时间是O(N^(7/6))
这个图感觉好形象的说。。。
上图是实现了序列为5, 2, 1的希尔排序的操作图示
/**
* 希尔排序
* 希尔排序的时间复杂度依赖于增量序列的优劣
* 希尔排序最坏的时间复杂度为: O(N^2)
* 采用Hibbard增量序列: 1, 3, 7, ....., 2^k - 1; 采用此增量序列的希尔排序的最坏运行时间是N^(5/4).
* @param data
*/
public static void hillSort(int[] data){
int i = 0, j = 0;
int H = (1 << (int) Math.log(data.length)) - 1; // 注意:"-"的优先级比"<<"高, 如果不加括号的话,会导致运算错误
for (; H >= 1; H = (H + 1) / 2 - 1){
// 内部是一个以H为间隔的子数组进行了插入排序
for (i = H; i < data.length; ++i){
j = i - H;
int temp = data[i];
while (j >= 0){
if (data[j] > temp){
data[j + H] = data[j];
j -= H;
}
else
break;
}
data[j + H] = temp;
}
}
}
/*********************************************************************************************************************************/
/*
希尔排序声明
希尔排序的时间复杂度依赖于增量序列的优劣
希尔排序最坏的时间复杂度为: O(N^2)
采用Hibbard增量序列: 1, 3, 7, ....., 2^k - 1; 采用此增量序列的希尔排序的最坏运行时间是N^(5/4).
sedgewick增量序列: 4^i - 3*2^i + 1; 采用此增量序列的希尔排序的最坏运行时间是O(N^(7/6))
*/
void ShellSort(int data[], int length)
{
int i = 0, j = 0, k = 0, H = 0, temp;
H = (1 << (int)_logb(length)) - 1; // 求取第一次要使用的增量值
for (; H >= 1; H = (H + 1)/2 - 1)
{
for (j = H; j < length; ++j) // 内部其实是一个插入排序
{
temp = data[j];
while (j >= 0){
if (temp < data[k]) {
data[k + H] = data[k];
j -= H;
}
else
break;
}
data[k + H] = temp;
}
}
}
/*********************************************************************************************************************************/
/**
* 归并排序
* 二路归并排序的时间复杂度为o(nlogn)
*/
public static void mergeSort(int[] data){
Msort(data, 0, data.length - 1);
}
/**
* 核心函数,通过递归实现将一个大数组分解为小数组,然后进行归并
* @param data
* @param leftStart
* @param rightEnd
*/
public static void Msort(int[] data, int leftStart, int rightEnd){
if (leftStart < rightEnd){
int middle = (leftStart + rightEnd) / 2; // 获取中间序号
Msort(data, leftStart, middle); // 先对左半部分进行归并排序
Msort(data, middle + 1, rightEnd); // 在对右半部份进行归并排序
merge(data, leftStart, middle + 1, rightEnd); // 将左半部分和右半部份进行合并
}
}
/**
* 合并两个数组中的数据,合并的前提是这两个数组都是有序的
* @param data
* @param leftStart
* @param rightStart
* @param rightEnd
*/
private static void merge(int[] data, int leftStart, int rightStart, int rightEnd){
int leftEnd = rightStart - 1;
int[] tempArray = new int[data.length];
int ls = leftStart, rs = rightStart, ts = leftStart;
// 关键步骤,通过每次比较两个数组的当前头节点来合并数组(前提是这两个数组都是有序的)
while (ls <= leftEnd && rs <= rightEnd){
if (data[ls] < data[rs])
tempArray[ts++] = data[ls++];
else
tempArray[ts++] = data[rs++];
}
// 左边剩下,拷贝剩余的部分
while (ls <= leftEnd)
tempArray[ts++] = data[ls++];
// 右边剩下, 拷贝剩余的部分
while (rs <= rightEnd)
tempArray[ts++] = data[rs++];
// 将tempArray中合并好的数组拷贝到data数组中
for (int i = rightEnd; i >= leftStart; --i)
data[i] = tempArray[i];
}
/*********************************************************************************************************************************/
// 归并排序
// 归并排序的最坏时间复杂度为:O(NlogN)
// 归并排序需要额外的空间. 大小等于原数组的大小N.
void MergeSort(int data[], int length)
{
void MSort(int data[], int TempArray[], int leftStart, int rightEnd);
int * TempArray = NULL;
TempArray = (int *)malloc(length * sizeof(int)); // 为temp数组分配空间.
if (TempArray == NULL)
{
cout << "可用空间不足, 无法进行排序" << endl;
return;
}
else
{
MSort(data, TempArray, 0, length - 1);
}
// 释放占用的空间
if (TempArray != NULL)
free(TempArray);
}
// 归并排序的主要操作, 递归进行归并
void MSort(int data[], int TempArray[], int leftStart, int rightEnd)
{
void Merge(int data[], int TempArray[], int leftStart, int RightStart, int RightEnd);
if (leftStart < rightEnd)
{
int Middle = (leftStart + rightEnd) / 2;
MSort(data, TempArray, leftStart, Middle); // 递归使用归并操作对左半数组排序
MSort(data, TempArray, Middle + 1, rightEnd); // 递归归并操作对右半数组排序
Merge(data, TempArray, leftStart, Middle + 1, rightEnd); // 左右半数组进行合并操作
}
}
// 归并排序用到的低级操作
// 合并操作: 将两个有序数组合并为一个
// 思想等同于以前写的两个有序链表的合并算法.
void Merge(int data[], int TempArray[], int leftStart, int RightStart, int RightEnd)
{
int leftEnd = RightStart - 1;
int dataNumber = RightEnd - leftStart + 1;
int lS = leftStart, rS = RightStart, tS = leftStart;
// 主和并循环
while (lS <= leftEnd && rS <= RightEnd)
{
if (data[lS] <= data[rS])
TempArray[tS++] = data[lS++];
else
TempArray[tS++] = data[rS++];
}
// 如果left数组剩下了, 则将剩下的部分直接复制到TempArray的后面
while (lS <= leftEnd)
TempArray[tS++] = data[lS++];
// 如果right数组剩下了, 则将剩下的部分直接复制到TempArray的后面
while (rS <= RightEnd)
TempArray[tS++] = data[rS++];
for (int i = RightEnd; i >= leftStart; --i)
data[i] = TempArray[i];
}
/*********************************************************************************************************************************/
两点注意:1. 分解: 选择一个枢纽值,将输入的序列array[m...n]划分成两个非空子序列array[m...k-1]和array[k+1...n],使得array[m...k-1]中的任一元素的值都不大于枢纽值, array[k+1...n]中的任一元素的值不小于枢纽值。2. 递归:通过递归的方式对array[m...k-1]和array[k+1...n]进行快速排序。3. 递归操作完成后整个数组就成为了一个有序数组。
Java实现1. 枢纽值的选取:枢纽值的选择越能将数组平均分开越好,反之将降低快速排序的运行效率. 如果每次都以数组第一个元素作为枢纽值时, 当数组元素有序时将得到最坏的运行效率o(n^2),所以这种方法是不允许的。 这里提供两种比较好的选择枢纽值得方式:第一种就是通过随机方式选取枢纽值, 但是随机选取需要用到随机数生成,这个代价还是比较昂贵的,一般采用第二个方法:那就是采用三数中值法。将首, 中, 尾位置上的记录进行比较,选择三者的中值作为枢纽值。这样的选取方式可以很好的工作。但是这种方式要求数组必须要有三个以上的元素。2. 有如下的事实:对于小数组(N<20), 快速排序不如插入排序。所以在递归中, 如果当前的数组的元素个数小于5, 我们就采取使用插入排序的方式进行排序,这样做有两个好处,既提高了效率, 同时也满足了三数中值法的要求。
/**
* 快速排序
* 平均时间复杂度为:o(nlogn)
* 最坏时间复杂度为:o(n^2)
* @param data
*/
public static void quickSort(int[] data){
Qsort(data, 0, data.length - 1);
}
/**
* 快速排序的核心函数
* 下面的算法采用了三数中值分割法, 尽可能好的选择了枢纽.
* 快速排序需要通过递归来实现,其最大的深度为logn + 1(向上取整), 所以空间复杂度应为logn
* @param data
* @param left
* @param right
*/
public static void Qsort(int[] data, int left, int right){
if (right - left > 5){ // 判断当前数组的大小, 如果当前数组过小, 则使用插入排序, 因为此时插入排序比快速排序要快
int pivot = middle3(data, left, right);
int i = left, j = pivot; // 因为最右边的那个数已经符合要求啦(因为在middle3中会使得最右边的那个数大于枢纽值)
for (;;){
while (data[++i] < data[pivot]); // 从左向右遍历元素, 遇到比枢纽值大的元素就在此停下来等待互换
while (data[--j] > data[pivot]); // 从右向左遍历元素, 遇到比枢纽值小的元素就在此停下来等待互换
if (i < j){ // 检查i 是否已经越过 j, 没有越过则需要交换两个位置上的值
swap(data, i, j);
}else{
break;
}
}
swap(data, i, pivot); // 将枢纽还原到原来的正确位置, 因为i在结束时肯定指向的是一个大于枢纽值的元素, 所以可以直接互换
Qsort(data, left, i - 1); // 左半边递归快排
Qsort(data, i + 1, right); // 右半边递归快排
}else{
// 当数组的个数小于3时,使用插入排序比使用快速排序效率更高
insertSortWithBound(data, left, right);
}
}
/**
* 三位中值分割法, 用于选取合适的枢纽, 是快速排序效率更高(相比于直接使用0号元素作为枢纽来进行排序, 三位中值分割法能更好的选取合适的枢纽值)
* 从头, 中, 尾三个元素中选出中间元素作为枢纽, 并将他们排序。返回值是枢纽元素所在的位置
* @param data
* @param left
* @param right
* @return
*/
private static int middle3(int[] data, int left, int right){
// 算出中间序号
int middle = (left + right) / 2;
// 将最前, 中间, 最后三个位置上的数据进行排序
if (data[left] > data[middle])
swap(data, left, middle);
if (data[left] > data[right])
swap(data, left, right);
if (data[middle] > data[right])
swap(data, middle, right);
// 将中间位置上的数据(枢纽值)和right-1位置的数据交换位置
swap(data, middle, right - 1);
return right - 1; // 交换位置后,right - 1 位置上的值才是枢纽值。
}
/*********************************************************************************************************************************/
// 快速排序
// 有以下的事实: 对于很小的数组(N <= 20), 快速排序不如插入排序好.
// 下面的算法采用了三数中值分割法, 尽可能好的选择了枢纽.
// 快速排序的平均情况的时间复杂度为: O(NlogN).
// 快速排序的最好情况的时间复杂度为: O(NlogN).
// 快速排序的最坏情况的时间复杂度为:O(N^2).
#define TRESHOLD 3
void QuickSort(int data[],int left, int right)
{
int Middle3(int data[], int left, int right);
void Swap(int* x, int* y);
//*1*
if (left + TRESHOLD <= right) // 判断当前数组的大小, 如果当前数组过小, 则使用插入排序, 因为此时插入排序比快速排序要快
{
int pivot = Middle3(data, left, right); // 只有在进行快排的时候才需要进行此操作, 在进行插入排序的时候则不需要此排序(如果在执行插入排序时进行了此操作会出错, 所以这个操作一定要放到if语句内部, 而不能放在外部, 即*1*处)
int i = left, j = pivot;
for (;;)
{
while (data[++i] < data[pivot]) ; // 从左向右遍历元素, 遇到比枢纽值大的元素就在此停下来等待互换
while (data[--j] >data[pivot]) ; // 从右向左遍历元素, 遇到比枢纽值小的元素就在此停下来等待互换
if (i > j) // 检查i 是否已经越过 j, 如果越过证明已经遍历完成
{
Swap(&data[i], &data[pivot]); // 将枢纽还原到原来的正确位置, 因为i在结束时肯定指向的是一个大于枢纽值的元素, 所以可以直接互换
break;
}
Swap(&data[i], &data[j]); // 如果i仍然小于j, 那么就需要将i停在的位置的元素和j停在的位置的元素进行互换. 让他们都在合适的位置
}
QuickSort(data, left, i - 1); // 左半边递归快排
QuickSort(data, i + 1, right); // 有半边递归快排
}
else
{
InsertSort(data + left, right - left + 1); // 当数组的个数不大于3时,使用插入排序, 因为小数组时时插入比快排更快
}
}
// 三位中值分割法, 用于选取合适的枢纽, 是快速排序效率更高(相比于直接使用0号元素作为枢纽来进行排序, 三位中值分割法能更好的选取合适的枢纽值)
// 从头, 中, 尾三个元素中选出中间元素作为枢纽, 并将他们排序
// 返回值是枢纽元素所在的位置
int Middle3(int data[], int left, int right)
{
void Swap(int* x, int* y);
int middle = (left + right)/2;
// 将这三个位置上的元素按照从小到大的顺序排序
if (data[left] > data[middle])
Swap(&data[left], &data[middle]);
if (data[left] > data[right])
Swap(&data[left], &data[right]);
if (data[middle] > data[right])
Swap(&data[middle], &data[right]);
// 将middle位置的枢纽和right - 1位置的元素互换
// 互换之后, right - 1的位置上的元素就是枢纽
int temp = data[middle];
data[middle] = data[right - 1];
data[right - 1] = temp;
return right - 1; // 返回middle位置
}
// 交换两个位置上的元素函数
void Swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
/*********************************************************************************************************************************/
/**
* 堆排序
* 堆排序的原理是先构建一个Max堆(大顶堆), 然后通过删除大顶堆的最大的节点(将根节点依次和最后一个节点进行互换位置进行删除操作).
* 最后排序完成(最后是升序排列的数据).
* 最坏时间复杂度: NlogN
* 平均时间复杂度: NlogN
* @param data
*/
public static void heapSort(int[] data){
// 首先将数组调整成为大顶堆
int length = data.length;
for (int i = length/2 - 1; i >= 0; --i){ // length/2 - 1的位置是最后一个有孩子的节点的位置
percolateDown(data, i, length - 1);
}
// 将大顶堆的顶点值和最后一个位置的值互换,然后再次进行下滤操作从剩余的数组中选出最大值, 重复该操作
for (int i = length - 1; i >= 0; --i){
swap(data, 0, i);
percolateDown(data, 0, i - 1);
}
}
/**
* 下滤操作, 堆排序所需的基本操作, 这个需要有完全二叉树的基本知识, 建议看一下树那一章。
* @param data
* @param i
* @param N
*/
public static void percolateDown(int[] data, int i, int N){
int temp, child;
for (temp = data[i]; leftChild(i) <= N; i = child){
child = leftChild(i);
if (child != N && data[child] < data[child + 1]){ // 如果有右孩子并且右孩子的值大于左孩子的值, 那么指向右孩子, 否则什么也不做
child++;
}
if (data[child] > temp){ // 最大的孩子和temp(爹)中的值进行比较,如果孩子的值比temp大,则爹和孩子互换位置, 如果孩子的值比temp小, 则停止循环
data[i] = data[child];
}else{
break;
}
}
data[i] = temp; // 将temp中的值赋给当前的位置
}
/**
* 计算左孩子的位置,因为数组的下标是从0开始的,所以计算左孩子下表的计算方法会和以1开始的数组的计算方式不太一样
* @param i
* @return
*/
private static int leftChild(int i) {
return 2*i+ 1;
}
/*********************************************************************************************************************************/
// 堆排序
// 堆排序的原理是先构建一个Max堆(大顶堆), 然后通过删除大顶堆的最大的节点(将根节点依次和最后一个节点进行互换位置进行删除操作.
// 最后排序完成(最后是升序排列的数据).
// 最坏时间复杂度: NlogN
// 平均时间复杂度: NlogN
void HeapSort(int data[], int length)
{
void PercolateDown(int data[], int i, int length);
// 第一步, 通过下滤操作构建Max堆
for(int i = length/2; i >= 0; --i)
PercolateDown(data, i, length);
cout << "完成Max堆的构建" << endl;
for (int j = length - 1; j > 0; --j) // 每次都将堆顶元素和当前堆的最后一个元素交换, 然后再次调整堆使之成为大顶堆
{
int temp = data[j];
data[j] = data[0];
data[0] = temp;
PercolateDown(data, 0, j);
}
}
// 堆排序所需要的下滤操作
#define LeftChild(i) ((2*i)+1) // 因为数组是以0开头的, 所以左孩子是2 * i + 1, 而右孩子是2 * i+ 2
void PercolateDown(int data[], int i, int length)
{
int temp, child;
for (temp = data[i]; (child = LeftChild(i)) < length; i = child)
{
if (child != length - 1 && data[child] < data[child + 1]) // 因为for中已经判断了child是小于length的, 所以在此只通过判断child是否等于length - 1就能判断出这个节点有没有右孩子
++child;
if (temp < data[child]) // 如果孩子中有比爹大的, 则孩子当爹.
data[i] = data[child];
else // 俩孩子都没有爹大
break;
}
data[i] = temp; // 下滤操作完成, 将temp填入合适的位置
}
/*********************************************************************************************************************************/
/*********************************************************************************************************************************/
// 桶式排序
// 桶式排序的要求比较苛刻, 它要求输入的数据必须只由小于max的正整数组成
// 桶式排序的原理很简单, 而且时间复杂度是线性的, 但是它对空间量需求比其它的排序算法要高的多.
// 桶式排序的平均时间复杂度是O(N)
void BucketSort(int data[], int length, int max)
{
int* temp = (int* )malloc(sizeof(int) * max);
if (temp == NULL)
{
cout << "内存空间不足" << endl;
return ;
}
// 初始化temp数组
for (int i = 0; i < max; ++i)
temp[i] = 0;
// 统计data数组中的元素
for (int i = 0; i < length; ++i)
temp[data[i]] += 1; // +=是考虑到可能会有重复的元素
for (int i = 0, j = 0; i < max; ++i)
while (temp[i]-- != 0)
{
data[j++] = i;
}
// 释放占用的空间
if (temp != NULL)
free(temp);
}
/*********************************************************************************************************************************/
排序算法 | 最好时间 | 平均时间 | 最坏时间 | 辅助存储 | 稳定性 | 备注 |
简单选择排序 | o(n2) | o(n2) | o(n2) | o(1) | 不稳定 | n小时较好 |
直接插入排序 | o(n) | o(n2) | o(n2) | o(1) | 稳定 | 基本有序时较好 |
冒泡排序 | o(n)(优化后) | o(n2) | o(n2) | o(1) | 稳定 | n小时较好, 基本有序时较好 |
希尔排序 | o(n) | o(nlogn) | o(ns)(1<s<2) | o(1) | 不稳定 |
希尔排序在使用不同的增量序列进行排序时, 其时间复杂度差别较大 s对于选择的不同的增量序列其最坏时间不尽相同 |
快速排序 | o(nlogn) | o(nlogn) | o(n2) | o(logn)(因为递归的原因) | 不稳定 |
使用三数中值分割法和小数组使用插入排序对快速排序进行优化, 使得快速排序得到很大的优化 待排数组越无序越好 |
堆排序 | o(nlogn) | o(nlogn) | o(nlogn) | o(1) | 不稳定 | |
归并排序 | o(nlogn) | o(nlogn) | o(nlogn) | o(n) (需要一个辅助数组) | 稳定 |
排序算法 | 平均情况下时间复杂度 | 最坏情况下时间复杂度 | 备注 | ||
插入排序 | O(N2) | O(N2) | |||
希尔排序 | Hibbard增量序列 | O(N5/4) | O(N3/2) | 希尔排序在使用不同的增量序列进行排序时, 其时间复杂度差别较大 | |
Sedgewick增量序列 | O(N7/6) | O(N4/3) | |||
堆排序 | O(NlogN) | O(NlogN) | 使用二叉堆, 先构建Max二叉堆, 然后使用DeleteMin方法每次将最大的数从队列中删除然后放到数组的尾部, 当执行N次后排序完成. | ||
归并排序 | O(NlogN) | O(NlogN) | 递归算法的优秀运用 | ||
快速排序 | O(NlogN) |
O(N2) |
使用三数中值分割法和小数组使用插入排序对快速排序进行优化, 使得快速排序得到很大的优化 |
||
桶式排序 | O(N) | - | 该算法使用限制较多, 但是在限制之下, 此算法可以以线性时间进行排序工作 |
标签:
原文地址:http://blog.csdn.net/wangmengdeboke/article/details/51897294