数据存储结构体,这里默认数据都是从下标1开始存储
const static int MAX = 1024;
// 存储数据的机构体
typedef struct
{
int r[MAX];
int length;
}SqList;
交换函数
// 交换两个数
void swap(SqList *L, int i,int j)
{
int tmp = L->r[i];
L->r[i] = L->r[j];
L->r[j] = tmp;
}
1.希尔排序
希尔排序其实就是插入排序的改进,插入排序可以看做每次比较的步长为1,而希尔排序设置一个初始的步长,步长迭代缩减,最后以1结束。希尔排序的复杂度主要和设置这个初始步长有关。当步长选择的比较差时复杂度就接近插入排序的复杂度。
这是插入排序的代码
void InsertSort(SqList *L)
{
int i,j;
for(i=2;i<=L->length;i++)// 从第二个数开始依次比较
{
if(L->r[i]<L->r[i-1])
{
L->[0] = L->r[i];// 将当前数记录在r[0]位置
// 循环和前边数比较,确定应该插入的位置,并将该位置后边的数都后移一位
for(j=i-1; L->r[j]>L->r[0];j--)
L->[j+1] = L->r[j];
L->r[j+1] = L->r[0];// 将该数插入对应位置
}
}
}
希尔排序则需要更改初始的比较步长,并在外部加一个循环,用来迭代步长,使步长最终减小为1.
希尔排序
void ShellSort(SqList *L)
{
int i,j;
int step = L->length;
do{
step = step/3+1;
for(i=step+1;i<=L->length;i++)// 从第step+1个数开始依次比较
{
if(L->r[i]<L->r[i-step])// 比较当前数和它前step位置的数大小
{
L->[0] = L->r[i];// 将当前数记录在r[0]位置
// 循环和前边相隔step个数比较,确定应该插入的位置
for(j=i-step; j>0 && L->r[j]>L->r[0];j-=step)
L->[j+step] = L->r[j];// 每次找到的比它大的数移动到后边位置
L->r[j+step] = L->r[0];// 将该数插入对应位置
}
}
}
while(step>1);
}
步长选取是希尔排序的关键,希尔排序是不稳定的排序算法,而且必须保证最后一次循环步长以1结束。
最好时间复杂度O(n),最坏O(n^2)
2.堆排序
堆是具有下列性质的完全二叉树:
- 每个节点的值都大于或等于其左右孩子的节点值,称为大顶堆;
- 每个节点的值都小于等于其左右孩子的节点值,称为小顶堆;
大顶堆和小顶堆使用层序遍历按顺序存储在数组中。
堆排序:利用大顶堆排序,首先将待排序的序列构造成一个大顶堆。此时堆顶元素就为最大的,将堆顶元素和末尾元素交换,然后调整前n-1个数为大顶堆,再将堆顶元素交换到n-1位置,继续将前n-2个数构调整成大顶堆。如此反复执行便能得到一个有序序列。
算法实现时主要需要解决两个问题:
- 由原始序列构建一个大顶堆
- 输出堆顶元素后,调整剩余元素为一个大顶堆
堆排序的代码如下
// 堆排序
void HeapSort(SqList *L)
{
int i = 0;
// 由初始序列构建大顶堆,从非叶子节点开始,从下到上调整每个非叶子结点
for (i = L->length / 2; i > 0; i--)
{
HeapAdjust(L, i, L->length);
}
// 依次取出堆顶元素调整大顶堆
for (i = L->length; i > 1; i--)
{
swap(L, 1, i); // 将堆顶元素换到末尾
HeapAdjust(L, 1, i - 1); // 将堆的元素个数进行-1,并将新交换的根节点重新调整堆为大顶堆
}
}
这里两个for循环,第一个是由初始给的数组序列构建一个大顶堆,第二个是每次讲大顶堆堆顶元素和末尾元素交换,并调整剩余元素为一个大顶堆。
其中void HeapAdjust(SqList *L, int s,int length)
函数是初始假设L中除s节点外都满足大顶堆条件,然后从s节点开始调整为一个合法的大顶堆,将s节点与左右孩子进行比较,如果孩子比节点s值大,则将较大的孩子和s交换位置,继续判断交换后的s节点和他的子节点,代码如下
// 调整大顶堆
void HeapAdjust(SqList *L, int s,int length)
{
// 初始假设L中除s节点外都满足大顶堆条件
int i = 0;
int tmp = L->r[s];
for (i = 2 * s; i < length;i*=2)
{
// 判断i是否出界而且判断左右孩子哪个比较大,让i指向大的孩子
if (i < length && L->r[i] < L->r[i + 1])
i++;
// 如果子结点的最大值小于需要判断的初始节点值,则表示初始节点放在该位置满足大顶堆条件,直接跳出
if (L->r[i] < tmp)
break;
// 将较大孩子的值赋给父节点
L->r[s] = L->r[i];
// 继续判断当前改动的节点对于他的孩子是否满足大顶堆条件,即记录下当前改动的节点
s = i;
}
// 将初始节点值放到对应位置
L->r[s] = tmp;
}
复杂度分析
运行时间主要消耗在初始简历大顶堆和后边调整大顶堆上,总体的时间复杂度为O(nlogn),最坏O(nlogn),最好O(nlogn)。
稳定性:由于记录的比较与交换是跳跃式的,所以堆排序是一种不稳定的排序算法。
由于初始时建堆时需要的比较次数较多,所以不适合待排序列个数较少的情况。
3.归并排序
时间复杂度:平均:O(nlogn),最好:O(nlogn),最坏:O(nlogn)
稳定性:稳定
归并排序原理:假设有n个数的序列需要排序,首先进行两两比较排序,然后将相邻的两两子序列进行归并排序,一直归并到n/2子列,然后对1-n/2子列和n/2+1到length子列进行归并,得到有序数列n,这种排序方法称为2路归并排序。
归并排序的递归实现
// 合并TR2中两个子序列s-m和m+1-t到数组TR1中
void Merge(int TR2[], int TR1[], int s, int m, int t)
{
int i, j, k;
for (i = s, j = m + 1,k=s; i <= m&&j <= t;k++)
{
if (TR2[i]>TR2[j])
{
TR1[k] = TR2[j];
j++;
}
else
{
TR1[k] = TR2[i];
i++;
}
}
if (i <= m)
{
for (int l = i; l <= m; l++,k++)
{
TR1[k] = TR2[l]; //将剩余的i-m复制到TR1中
}
}
if (j <= t)
{
for (int l = j; l <= t; l++, k++)
{
TR1[k] = TR2[l]; // 将剩余的m+1 -- t复制到TR1中
}
}
}
//SR数组归并排序为TR1数组
void Msort(int SR[], int TR1[], int s, int t)
{
int TR2[MAX];
if (s == t)
TR1[s] = SR[s];
else
{
int m = (s + t) / 2; // 将SR[s...t]平分为SR[s...m]和SR[m+1...t]
Msort(SR, TR2, s, m); // 递归对SR[s...m]进行归并排序为TR2
Msort(SR, TR2, m + 1, t); // 递归对SR[m+1...t]进行归并排序为TR2
Merge(TR2, TR1, s, m, t); // 实际的归并排序 将TR2中s到m和m+1到t归并排序为TR1
}
}
// 归并排序
void MergeSort(SqList *L)
{
Msort(L->r, L->r, 1, L->length);
}
非递归实现
// 将数组SR中长度为k的两个相邻子列进行归并放入TR数组中
void MergePass(int SR[], int TR[], int k, int length)
{
int i = 1;
while (i <= length - (2 * k)+1)
{
Merge(SR, TR, i, i + k - 1, i + 2 * k - 1);
i = i + 2 * k;
}
// 归并最后两个序列
if (i < length - k + 1)
{
Merge(SR, TR, i, i + k - 1, length);
}
else
{
// 剩下单个子序列
for (int j = i; j <= length; j++)
{
TR[j] = SR[j];
}
}
}
// 归并排序非递归实现
void MergeSort2(SqList *L)
{
int *TR = (int *)malloc(sizeof(int)*(L->length + 1)); // 申请一个辅助空间
int k = 1;
while (k < L->length)
{
MergePass(L->r, TR, k, L->length);
k = k * 2;
MergePass(TR, L->r, k, L->length);
k = k * 2;
}
}
4.快速排序
思想:快速排序通过一趟排序将数列划分为两个部分,前边的都比选中的关键字小,后边的都比选中的关键字大,然后继续对前后两部分分别进行排序,以达到整个序列有序。
递归实现代码如下
// 将L-r中low到high划分为两部分,并返回划分点的位置
int Partition(SqList *L, int low, int high)
{
int pivotKey = L->r[low]; // 用子序列的第一个作为划分点
// 进行划分:这里使用两端交替向中间划分
while (low < high)
{
while (low < high && pivotKey <= L->r[high])
high--;
swap(L, low, high);
while (low < high && pivotKey >= L->r[low])
low++;
swap(L, low, high);
}
return high;
}
// 对表中序列r[low-high]进行快速排序
void Qsort(SqList *L, int low, int high)
{
int pivot;
if (low < high)
{
pivot = Partition(L, low, high); // 将L->r[low-high]一分为二,前边都比L->r[pivot]小,后边都比这个大
Qsort(L, low, pivot - 1); // 对low - pivot-1这部分继续进行递归划分
Qsort(L, pivot + 1, high); // 进行递归划分
}
}
// 快速排序
void QuickSort(SqList *L)
{
Qsort(L, 1, L->length);
}
最坏情况下复杂度为O(n^2),平均时间复杂度为O(nlogn),最好O(nlogn)
稳定性:不稳定