算法分析
希尔排序
对于大规模乱序数组插入排序很慢,效率较低,例如最小数在数组最右,要将其挪到正确位置就要N-1次移动。希尔排序为了加快速度简单的改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。
步骤
1.使用元素总数量的一半5作为增量,将元素划分为五个序列,对这五个序列分别进行排序;
2.使用5/2=2作为增量,将元素划分成两个序列分别排序;
3.最后使用插入排序,将元素排序,结果为:
优劣:
- 不需要大量的辅助空间,和归并排序一样容易实现;
- 希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n²),而Hibbard增量的希尔排序的时间复杂度为O(n3/2),希尔排序时间复杂度的下界是n*log2n;
- 希尔排序没有快速排序算法快 O(n*logn),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择;
- 希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,而快速排序在最坏的情况下执行的效率会非常差。
快速排序(挖坑填数+分治法)
快速排序(Quick Sort)是对冒泡排序的一种改进,基本思想是选取一个记录作为枢轴,经过一趟排序,将整段序列分为两个部分,其中一部分的值都小于枢轴,另一部分都大于枢轴。然后继续对这两部分继续进行排序,从而使整个序列达到有序。
快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(n²),它的平均时间复杂度为O(n*logn)。
步骤
堆排序
作为选择排序的改进版,堆排序可以把每一趟元素的比较结果保存下来,以便我们在选择最小/大元素时对已经比较过的元素做出相应的调整。
堆排序是一种树形选择排序,在排序过程中可以把元素看成是一颗完全二叉树,每个节点都大(小)于它的两个子节点,当每个节点都大于等于它的两个子节点时,就称为大顶堆,也叫堆有序; 当每个节点都小于等于它的两个子节点时,就称为小顶堆。
算法思想(以大顶堆为例):
1.将长度为n的待排序的数组进行堆有序化构造成一个大顶堆;
2.将根节点与尾节点交换并输出此时的尾节点;
4.重复第二、三步直至构造成一个有序序列。
归并排序
归并排序算法有两个基本的操作,一个是分,也就是把原数组划分成两个子数组的过程。另一个是治,它将两个有序数组合并成一个更大的有序数组。
它将数组平均分成两部分: center=(left+right)/2,当数组分得足够小时即数组中只有一个元素时,只有一个元素的数组自然而然地就可以视为是有序的,此时就可以进行合并操作了。因此,上面讲的合并两个有序的子数组,是从只有一个元素的两个子数组开始合并的,合并后的元素个数:从 1->2->4->8……
由归并排序的递归公式:T(n)=2T(n/2)+O(n)可知时间复杂度为O(n*logn)。
此外,归并排序中的比较次数是所有排序中最少的。原因是,它一开始是不断地划分,比较只发生在合并各个有序的子数组时。
步骤
程序代码
#pragma once
#include <time.h>
#include <iostream>
#include <fstream>
using namespace std;
#ifndef _SORT_H
#define _SORT_H
struct Record
{
int key;
};
#endif
//产生随机数
Record* GenerateData(Record r[], int n)
{
srand((unsigned)time(NULL));
for(int i = 0; i < n; i++)
{
r[i].key = rand() % n; //生成n个取值为[0,n-1]的整数
}
return r;
}
//写入文件
Record* CreateFile(Record r[], int n, const char* filename)
{
fstream fout(filename, ios::out);
if (!fout)
{
cout << "无法创建文件!" << endl;
exit(1);
}
else
{
for (int i = 0; i < n; i++)
{
fout << r[i].key << " ";
if ((i + 1) % 15 == 0)
fout << endl;// '\n';
}
}
fout.close();
return r;
}
//读取文件
Record* ReadFile(int n, const char* filename)
{
fstream fin(filename, ios::in);
Record* r = new Record[n];
if (!fin)
{
cout << "无法打开文件!" << endl;
exit(1);
}
else
{
for (int i = 0; i < n; i++)
fin >> r[i].key;
}
fin.close();
return r;
}
#include <iostream>
#include <fstream>
#include "Sort.h"
#include "File.h"
#define N 10000
using namespace std;
int Menu();
void Procedure();
int Menu()
{
cout << "__________*******************************************************__________\n\n";
cout << " 1.产生随机数作为待排序的数据,并将其存入磁盘文件中\n";
cout << " 2.执行【希尔排序】算法,将排序结果存入文件\n";
cout << " 3.执行【快速排序】算法,将排序结果存入文件\n";
cout << " 4.执行【堆排序】算法, 将排序结果存入文件\n";
cout << " 5.执行【归并排序】算法,将排序结果存入文件\n";
cout << " 0.退出操作\n\n";
cout << "__________*******************************************************__________\n\n";
cout << "请依次输入以上5个步骤的代号: ";
Sleep(2000);
int choice;
cin >> choice;
cout << endl;
return choice;
}
void Procedure()
{
while (1)
{
int choice = Menu();
if (choice == 0)
break;
switch (choice)
{
case 1:
{
cout << "提示:本程序中待排序数据的个数为" << N << endl;
Record* r = new Record[N];
GenerateData(r, N);
Sleep(1000);
CreateFile(r, N, "随机数.txt");
Sleep(1000);
cout << "数据已存入【随机数.txt】文件\n\n";
Sleep(1500);
break;
}
case 2:
{
Record* rec = ReadFile(N, "随机数.txt");
Sleep(1000);
ShellSort(rec, N);
cout << "对待排序数据进行希尔排序\n\n";
Sleep(1000);
CreateFile(rec, N, "希尔排序.txt");
cout << "排序的结果已存入【希尔排序.txt】文档\n\n";
Sleep(1500);
break;
}
case 3:
{
Record *rec = ReadFile(N, "随机数.txt");
Sleep(1000);
QuickSort(rec, 0, N - 1);
cout << "对待排序数据用快速排序方法进行排序\n\n";
Sleep(1000);
CreateFile(rec, N, "快速排序.txt");
cout << "排序的结果已存入【快速排序.txt】文档\n\n";
Sleep(1500);
break;
}
case 4:
{
Record* rec = ReadFile(N, "随机数.txt");
Sleep(1000);
HeapSort(rec, N);
cout << "对待排序数据用堆排序方法进行排序\n\n";
Sleep(1000);
CreateFile(rec, N, "堆排序.txt");
cout << "排序的结果已存入【堆排序.txt】文档\n\n";
Sleep(1500);
break;
}
case 5:
{
Record* rec = ReadFile(N, "随机数.txt");
Sleep(1000);
MergeSort(rec, N);
cout << "对待排序数据用归并排序方法进行排序\n\n";
Sleep(1000);
CreateFile(rec, N, "归并排序.txt");
cout << "排序的结果已存入【归并排序】.txt文档\n\n";
Sleep(1500);
break;
}
default:
cout << "输入错误!\n\n";
}
}
}
int main()
{
Procedure();
return 0;
}
#pragma once
#include <iostream>
#include <time.h>
#include <windows.h>
using namespace std;
#ifndef _SORT_H
#define _SORT_H
struct Record
{
int key;
};
#endif
//希尔排序算法
void ShellSort(Record r[], int n)
{
for (int d = n / 2; d >= 1; d = d / 2)
{
for (int i = d; i < n; i++)
{
Record temp = r[i];
int j;
for (j = i - d; j >= 0 && temp.key < r[j].key; j = j - d)
r[j + d] = r[j];
r[j + d] = temp;
}
}
}
//快速排序的一次划分算法
int Partition(Record r[], int i, int j)
{
Record temp = r[i];
while (i < j)
{
while (i < j && r[j].key >= temp.key)
j--;
if (i < j)
r[i++] = r[j];
while (i < j && r[i].key <= temp.key)
i++;
if (i < j)
r[j--] = r[i];
}
r[i] = temp;
return i;
}
//快速排序算法
void QuickSort(Record r[], int i, int j)
{
if (i < j)
{
int pivot = Partition(r, i, j);
QuickSort(r, i, pivot - 1);
QuickSort(r, pivot + 1, j);
}
}
//堆排序中的筛选算法
void Sift(Record r[], int k, int m)
{
int i = k;
int j = 2 * i + 1; //置i为要筛的结点,j为i的左孩子
while (j <= m) //筛选还没进行到叶子
{
if (j < m && r[j].key < r[j + 1].key)
j++; //比较i的左右孩子,j为较大者
if (r[i].key > r[j].key)
break; //根结点已经大于左右孩子中的较大者
else
{
Record temp = r[i];
r[i] = r[j];
r[j] = temp;
i = j;
j = i * 2 + 1; //被筛选点位于原来结点j的位置
}
}
}
//堆排序算法
void HeapSort(Record r[], int n)
{
for (int i = n / 2 - 1; i >= 0; i--) //初始建堆,从最后一个非终端结点至根结点进行筛选
Sift(r, i, n - 1);
for (int i = 1; i < n; i++) //重复执行移走堆顶及重建堆的操作
{
Record temp = r[0];
r[0] = r[n - i];
r[n - i] = temp;
Sift(r, 0, n - i - 1);
}
}
//一次归并算法
void Merge(Record r[], Record r1[], int s, int m, int t)
{
int i = s;
int j = m + 1;
int k = s;
while (i <= m && j <= t)
if (r[i].key <= r[j].key)
r1[k++] = r[i++]; //取r[i]和r[j]中较小者放入r1[k]
else
r1[k++] = r[j++];
if (i <= m)
while (i <= m)
r1[k++] = r[i++]; //若第一个子序列没处理完,则进行收尾处理
else
while (j <= t)
r1[k++] = r[j++]; //若第二个子序列没处理完,则进行收尾处理
}
//一趟归并排序算法
void MergePass(Record r[], Record r1[], int n, int h)
{
int i = 0;
while (i <= n - 2 * h + 1) //待归并记录至少有两个长度为h的子序列
{
Merge(r, r1, i, i + h - 1, i + 2 * h - 1);
i += 2 * h;
}
if (i < n - h + 1)
Merge(r, r1, i, i + h - 1, n); //待归并序列中有一个长度小于h
else
for (int k = i; k <= n; k++)
r1[k] = r[k]; //待归并序列中只剩下一个子序列
}
//归并排序的非递归算法
void MergeSort(Record r[], int n)
{
int h = 1;
Record* r1 = new Record[n];
while (h < n)
{
MergePass(r, r1, n - 1, h);
h = 2 * h;
MergePass(r1, r, n - 1, h);
h = 2 * h;
}
}
经验总结
- 从平均时间来看,快速排序是效率最高的,但快速排序在最坏情况下的时间性能不如堆排序和归并排序。而后者相比较的结果是,在n较大时归并排序使用时间较少,但使用辅助空间较多;
- 上面说的简单排序包括除希尔排序之外的所有冒泡排序、插入排序、简单选择排序。其中直接插入排序最简单,但序列基本有序或者n较小时,直接插入排序是好的方法,因此常将它和其他的排序方法,如快速排序、归并排序等结合在一起使用;
- 快速排序、堆排序、希尔排序等时间性能较好的排序方法都是不稳定的。稳定性需要根据具体需求选择;
- 上面的算法实现大多数是使用线性存储结构,像插入排序这种算法用链表实现更好,省去了移动元素的时间。具体的存储结构在具体的实现版本中也是不同的。