1. 排序的基本概念
1 排序概念
排序:将一组数据元素序列重新排序,使得数据元素序列按某个数据项(关键字)有序。
排序依据:是依据数据元素的关键字。
若关键字是主关键字(关键字值不重复),则无论采用何种排序方法,排出的结果都是唯一的。
若关键字是次关键字(关键字值可以重复),则排出的结果可能不唯一。
2 稳定排序和不稳定排序方法
稳定的排序方法:对于任意的数据元素序列,排序前后所有相同关键字的相对位置都不变。
不稳定的排序方法:存在一组数据序列,在排序前后,相同关键字的相对位置发生了变化。
- 排序的过程是一个逐步扩大记录的有序序列长度的过程。
2. 插入排序
基本思想:将无序子序列中的一个或几个记录“插入”到有序子序列中,从而增加有序子序列的长度。
插入排序三部曲:
1 直接插入排序
排序过程:整个排序过程为 n-1 趟插入,即先将序列中第1个记录看成是一个有序子序列,然后从第2个记录开始,逐个进行插入,直至整个序列有序。
实例:
算法实现:
typedef struct
{
int key;
float info;
}JD;
void straisort(JD r[], int n) //对长度为 n 的序列排序
{
int i, j;
for(i=2;i<=n;i++) //数组从第1位开始存数据
{
r[0] = r[i];
j = i-1;
while(r[0].key < r[j].key)
{
r[j+1] = r[j];
j--;
}
r[j+1] = r[0];
}
}
性能分析:(比较次数和移动次数)
简单插入排序的本质:比较和交换
- 序列中逆序的个数,决定交换次数
- 平均逆序数量为 C(n,2)/2 ,所以 T(n) = O(n2)
- 简单插入排序复杂度由逆序个数决定
如何改进简单插入排序复杂度:(希尔排序)
2 希尔排序(缩小增量法)
1) 基本思想
基本思想:将一组待排序的数据分割成若干个较小的子文件,对各个子文件分别进行直接插入排序,当文件达到基本有序时,再对整个文件进行一次直接插入排序。
对待排记录序列先作“宏观”调整,再作“微观”调整。
“宏观”调整指的是:“跳跃式”的插入排序。
2) 排序过程
- 首先将记录序列分成若干个子序列
- 然后分别对每个子序列进行直接插入排序
- 最后待基本有序时,再进行一次直接插入排序
- 其中 d 称为增量,它的值在排序过程中从大到小逐渐缩小,直至最后一趟排序减为1。
3)算法的实现
void ShellSort(JD r[], int n, int d[], int T)
{
//r[]为待排序数据,一共有n个;d[]为增量数组,有T个
int i,j,k;
JD x;
k = 0;
while(k < T)
{
for(i=d[k]+1;i<=n;i++) //i+1则进行下一组的排序
{
//i为未排序记录的位置
x = r[i]; //将未排序的数据拷贝给x
j = i-d[k]; //j为本组位置i前面的一个记录的位置
while((j > 0)&&(x.key < r[j].key))
{
//组内简单插入排序
r[j+d[k]] = r[j];
j = j-d[k];
}
r[j+d[k]] = x;
}
k++;
}
}
4) 希尔排序的特点
- 最坏时间复杂度:
3. 选择排序
1 简单选择排序
1) 基本思想
基本思想:从无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。
2) 排序过程
- 首先通过 n-1 次关键字比较,从 n 个记录中找出关键字最小的记录,将它与第一个记录交换。
- 再通过 n-2 次比较,从剩余的 n-1 个记录中找出关键字次小的记录,将它与第二个记录交换。
- 重复上述操作,共进行 n-1 趟排序后,排序结束。
3) 算法的实现
void smp_selesort(JD r[], int n)
{
int i, j, k;
JD x;
for(i=1;i<n;i++)
{
// i 表示第 i 趟,同时也表示最小元素应该放的位置
k = i;
for(j=i+1;j<=n;j++)
{
if (r[j].key < r[k].key)
{
k = j; //k 用来标记未排序记录中的最小数据位置
}
}
if (i != k)
{ //交换r[i]和r[k]
x = r[i];
r[i] = r[k];
r[k] = x;
}
}
}
4) 性能分析
稳定性分析:
- 不稳定的排序
2 归并排序
- 将两个或两个以上的有序表组合成一个新的有序表,叫归并排序。
2-路归并排序的排序过程:
- 设初始序列含有 n 个记录,则可看成 n 个有序的子序列,每个子序列长度为1。两两合并,得到 个长度为2或1的有序子序列。再两两合并,......如此重复,直至得到一个长度为 n 的有序序列为止。
实例:
迭代算法:
- 将序列的每一个数据看成一个长度为1的有序表,
- 然后,将相邻两组进行归并得到长度为2的有序表(一趟归并)
- 再对相邻两组长度为2的有序表进行下一趟归并得到长度为4的有序表
- 这样一直进行下去,直到整个表归并成有序表。
- 如果某一趟归并过程中,单出一个表,该表轮空,等待下一趟归并。
递归思想:
- 将无序序列划分成大概均等的2个子序列,然后用同样的方法对2个子序列进行归并排序得到2个有序的子序列,再用合并2个有序表的方法合并这2个子序列,得到 n 个元素的有序序列。
void Merge(JD A[], JD TmpArray[], int Lpos, int Rpos, int RightEnd )
{
//Lpos为左半部分的开始位置,Rpos为右半部分的开始位置
int i,LeftEnd,NumElements,TmpPos;
LeftEnd = Rpos-1; //左半部分的结束位置
TmpPos = Lpos; //新数组的位置
NumElements = RightEnd - Lpos + 1; //元素个数
while(Lpos <= LeftEnd && Rpos <= RightEnd)
{
if (A[Lpos] <= A[Rpos]) //谁小谁拷贝
{
TmpArray[TmpPos++] = A[Lpos++];
}
else
TmpArray[TmpPos++] = A[Rpos++];
}
while(Lpos <= LeftEnd) //左半部分还有元素
TmpArray[TmpPos++] = A[Lpos++];
while(Rpos <= RightEnd) //右半部分还有元素
TmpArray[TmpPos++] = A[Rpos++];
for(i=0;i<NumElements;i++,RightEnd--)
{ //从额外的空间拷贝回数组A
A[RightEnd] = TmpArray[RightEnd];
printf("%d",A[RightEnd]);
}
}
void MSort(JD A[], JD TmpArray[], int Left, int Right)
{
//A[]为待排序的数据,TmpArray[]是长度与A[]相同的额外的数组空间,起始地址:Left;结束地址:Right
int Center;
if (Left < Right) //待排序的数据在数组的下标位置
{
Center = (Left + Right)/2;
MSort(A,TmpArray,Left,Center); //T(N/2) 递归
MSort(A,TmpArray,Center+1,Right); //T(N/2) 递归
Merge(A,TmpArray,Left,Center+1,Right); //O(N)
}
}
void Mergesort(JD A[], int N)
{
JD *TmpArray; //额外的O(N)空间
TmpArray = malloc(N*sizeof(JD)); //单独写一个函数,实现只需分配一次额外的空间
if (TmpArray!=NULL)
{
MSort(A,TmpArray,0,N-1);
free(TmpArray);
}
else
FatalError("No space for tmparray");
}
算法评价:
- 时间复杂度:每一趟归并的时间复杂度为O(N),总共需要归并 logN 趟,因而,总的时间复杂度为 O(NlogN)。
- 空间复杂度:2-路归并排序过程中,需要一个与表等长的存储单元数组空间,因此,空间复杂度为 O(N)。
4. 交换排序
通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。交换排序包括:
- 冒泡排序
- 快速排序
1 冒泡排序
1) 排序过程
2) 算法实现
void bubble_sort(JD r[], int n)
{
int m, i, j, flag = 1; //flag为每趟是否有交换操作的标识,flag = 1表示有交换操作
JD x;
m = n; //m指向每趟未排序序列的最后位置
while((m>1) && (flag == 1))
{
flag = 0; //将标识设为没有交换操作
for (j=1;j<m;j++)//本趟将最大元素放到未排序序列的最后位置
{
if (r[j].key > r[j+1].key) //前面比后面数据大则交换
{
flag = 1; //标识置为有交换操作
x = r[j];
r[j] = r[j+1];
r[j+1] = x;
}
}
m--;
}
}
3) 性能分析
4) 冒泡排序的改进
- 冒泡排序的结束条件为:最后一趟没有进行“交换记录”。
- 一般情况下,每经过一趟“冒泡”,“m 减1”,但并不是每趟都如此。增加一个变量记录最后进行交换的位置。
void bubble_sort(JD r[], int n)
{
int m, i, j;
int lastExchangeIndex; //记录最后一次进行交换的位置
JD x;
m = n; //m指向每趟未排序序列的最后位置
while(m > 1)
{
lastExchangeIndex = 1; //每趟都需赋值为1
for (j=1;j<m;j++)//本趟将最大元素放到未排序序列的最后位置
{
if (r[j].key > r[j+1].key) //前面比后面数据大则交换
{
x = r[j];
r[j] = r[j+1];
r[j+1] = x;
lastExchangeIndex = j; //记录下进行交换的记录位置
}
}
m = lastExchangeIndex; //本趟最后一次交换的位置,后面没有进行交换的说明已经排好序了
}
}
2 快速排序
1) 算法思想
2) 枢纽的选择
3) 排序过程
void qksort(JD r[], int t, int w)
{
//t=low,w=high
int i, j, k;
JD x;
if(t >= w)
return;
i = t;
j = w;
x = r[i]; //枢纽元素
while(i < j)
{
while((i < j) && (r[j].key >= x.key)) //枢纽后面的值大于枢纽
j--;
if (i < j)
{ //当发现枢纽后面比枢纽小的数后,与枢纽进行交换
r[i] = r[j];
i++;
}
while((i < j) && (r[i].key <= x.key)) //枢纽前面的值小于枢纽
i++;
if (i < j)
{ //当发现枢纽前面比枢纽大的数后,与枢纽进行交换
r[j] = r[i];
j--;
}
//缺点是需要将枢纽元素进行多次交换
}
r[i] = x;
qksort(r,t,j-1);
qksort(r,j+1,w);
}
4) 快速排序的改进
- 三者取中法 和 不交换枢纽元素
//交换函数
void swap(JD &a, JD &b)
{
JD x = a;
a = b;
b = x;
}
//三数中值函数
const JD & median3(JD A[], int left, int right)
{
int center = (left + right)/2;
if(A[center].key < A[left].key)
swap(A[left],A[center]);
if(A[right].key < A[left].key)
swap(A[left],A[right]);
if(A[right].key < A[center].key)
swap(A[center],A[right]);
//将中值放在最后
swap(A[center],A[right]);
return A[right];
}
void quicksort(JD A[], int left, int right)
{
if (left+10 <= right) //元素超过10个才用快排,否则用直接插入排序
{
JD pivot = median3(A,left,right);
int i = left, j = right-1;
for( ; ; )
{
while(A[i].key < pivot.key)
i++;
while(A[j].key > pivot.key)
j--;
if(i < j)
swap(A[i],A[j]);
else
break;
}
swap(A[i],A[right]); //将枢纽元素放回中间
quicksort(A,left,i-1);
quicksort(A,i+1,right);
}
else
straisort(A,right-left+1); //元素少时用直接插入排序
}
5) 快速排序算法的特点
5. 基数排序(多关键字排序)
- 高位优先多关键字排序
- 低位优先多关键字排序
1 高位优先多关键字排序
- 先对 进行排序,并按 的不同值将记录序列分成若干子序列之后
- 在子序列中 相同的情况下,分别对 进行排序,......
- 依次类推,直至对最次位关键字 排序完成为止。
- 关键字的优先级递减。
2 低位优先多关键字排序
- 先对关键字 进行排序,并按 的不同值将记录序列分成若干子序列之后
- 在子序列中 相同的情况下,分别对关键字 进行排序,......
- 依次类推,直至对最高位关键字 排序完成为止。
- 关键字的优先级递减。
3 链式基数排序操作步骤
const int M=10;
struct radix_sort
{
int number;
radix_sort* next;
};
//桶排序
template<int t>
void bucket_sort(int (&a)[t]) //数组的引用做形参时必须指定大小,但如果大小需要改变时,可以使用模板
{
int sort[M]={0};
int count;
int n=sizeof(a)/sizeof(int);
for (int i=0;i<n;i++)
{
sort[a[i]]+=1;
}
for(int j=0;j<M;j++)
{
if(sort[j]!=0)
{
count=sort[j];
for (int k=0;k<count;k++)
{
cout<<j<<" ";
}
}
}
cout<<endl;
}
//基数排序
template<int t>
void Radix_Sort(int (&a)[t],int b,int p)//b为1,表示从个位开始,b表示目前的桶排序位置,p表示一共的位数
{
radix_sort* Sort[M]={NULL};
int j,r;
int n=sizeof(a)/sizeof(int);
for (int i=0;i<n;i++)
{
r=a[i];
int d=b;
while(d--)//如64,4=64%10,6=(64/10)%10;用于提取该位(循环直到取到该位结束)
{
j=r%10;
r=r/10;
}
if (Sort[j]==NULL)
{
radix_sort* p3=new radix_sort;
p3->number=a[i];
p3->next=NULL;
Sort[j]=p3;
}
else
{
radix_sort* p=new radix_sort;
p->number=a[i];
p->next=NULL;
radix_sort* p1=Sort[j];
while(p1->next!=NULL)
{
p1=p1->next;
}
p1->next=p;
}
} //用链表存储目前位相同的数据
int c=0;
for(int k=0;k<M;k++)
{
if (Sort[k]!=NULL)
{
radix_sort* p2=Sort[k];
while(p2->next!=NULL)
{
a[c++]=p2->number;
p2=p2->next;
}
a[c++]=p2->number;
}
} //把该位的排序结果赋值给数组a
if (b!=p)
{
Radix_Sort(a,++b,p);//从个位开始依次递归
}
else
{
for(int q=0;q<n;q++)
{
cout<<a[q]<<" ";
}
cout<<endl;
}
}
- 数组的引用做形参时必须指定大小,但如果大小需要改变时,可以使用模板