目录
1.排序基本概念
01关键词
02什么是排序
03排序的稳定性
04内排序和外排序
05算法的性能
06排序数据组织
2.插入排序
01直接插入排序
02折半插入排序
03希尔排序
3.交换排序
01冒泡排序
02快速排序
4.选择排序
01简单选择排序
02堆排序
5.归并排序
6.基数排序
7.各种排序比较
在排序算法中,关键词是指用于比较和排序的数据元素。在排序过程中,算法根据关键词的大小关系来决定元素的排列顺序。具体来说,对于数字类型的数据,通常使用数字本身作为关键词进行比较排序;而对于字符串类型的数据,则可以使用字典序或者长度等属性作为关键词进行排序。在排序算法中,关键词的选择很关键,它直接影响排序的效率和结果。因此,在实际应用中,需要根据具体场景来选择合适的关键词进行排序。
下面以数字排序为例,说明关键词的概念和作用。
假设有以下一组数字需要进行升序排序:3,9,1,8,5
其中,数字本身就是关键词,按照数字大小进行排序即可。我们可以使用冒泡排序算法来完成排序过程。
第一趟排序:
比较3和9,3小于9,不需要交换位置; 比较9和1,9大于1,交换位置,变成3,1,9,8,5; 比较9和8,9大于8,交换位置,变成3,1,8,9,5; 比较9和5,9大于5,交换位置,变成3,1,8,5,9;
第一趟结束后,最大的数字9已经排到了最后,接下来再从3,1,8,5四个数字中继续找出最大值,放到倒数第二个位置。这时我们发现每次比较时都需要对数字进行大小判断,因此“数字大小”就是这里的关键词。
排序是将一组数据按照特定的规则或算法,使得数据按照指定的顺序排列的过程。常见的排序方式包括冒泡排序、插入排序、选择排序、快速排序、归并排序等。排序可以用于对数据进行查找、分析和统计等操作。
排序的稳定性指的是对于具有相同关键字的元素,在排序前后它们的相对位置是否发生了改变。若排序前和排序后,相等的数的相对位置不变,则称该排序算法是稳定的;否则称为不稳定的。
例如,有一组数据 {3, 2, 3, 1},如果使用非稳定排序算法对其进行排序,可能得到 {1, 2, 3, 3} 的结果,这时候第一个 3 和第二个 3 的相对位置发生了改变。而如果使用稳定排序算法,则可以保证在排序后第一个 3 在第二个 3 的前面,即 {2, 3, 3, 1}。
稳定性对于某些应用场景非常重要,比如对于基于关键字的快速查找、去重等操作,需要保持元素在数组中的相对位置不变。因此,在选择排序算法时,需要根据实际需求来选择是否需要考虑排序的稳定性。
内排序:在排序过程中,若整个表都是放在内存中处理,排序时不涉及数据的内、外存交换,则称之为内排序。它的运行时间取决于数据本身的特征,空间复杂度是O(1),因此其执行效率比较高,但是对于处理大量数据时,会有内存不足的问题。
外排序:若排序过程中要进行数据的内、外存交换,则称之为外排序。它的运行时间主要受限于磁盘I/O速度,空间复杂度取决于分块大小和缓存区大小。它适用于处理大量数据的情况,但相对于内排序而言,它的执行效率会降低。
算法的性能主要由时间复杂度和空间复杂度两个方面来衡量。
时间复杂度:表示算法执行所需要的时间,通常用“大O符号”来表示。它是根据算法中基本操作的执行次数来计算的。时间复杂度越小,算法执行效率越高。
空间复杂度:表示算法执行所需要的内存空间大小。通常也用“大O符号”来表示。空间复杂度越小,算法所占用的内存空间越少。
除了时间复杂度和空间复杂度之外,还有其他因素会影响算法的性能,如硬件设备、编程语言、编译器、优化技术等。
因此,在实际应用中,需要综合考虑算法的时间复杂度、空间复杂度以及其他因素,来选择最优算法。
时间效率——排序速度(即排序所花费的全部比较次数)。
空间效率——占内存辅助空间的大小。
稳定性——若两个记录A和B的关键字值相等,若排序后A、B的先后次序保持不变,则称这种排序算法是稳定的。
typedef int KeyType; //定义关键字类型为int
typedef struct //元素类型
{
KeyType key; //关键字项
}RecType //排序元素的类型
主要的插入排序方法:
(1)直接插入排序 (2)折半插入排序 (3)希尔排序
1.排序思路:
直接插入排序是一种简单的排序算法,其基本思想是将待排序的序列分为两部分:有序区和无序区。初始时,有序区只包含一个元素,即序列的第一个元素,而无序区则包含除第一个元素以外的所有元素。排序过程中,每次从无序区取出第一个元素,将它插入到有序区的合适位置上,直到无序区为空,排序完成。
具体的实现步骤如下:
从第二个元素开始遍历待排序序列。
将当前元素存储在临时变量中,即将该元素从无序区中移出。
从后往前遍历有序区,如果有序区中的某个元素大于当前元素,则将该元素向后移动一个位置,直到找到第一个小于等于当前元素的位置。
将临时变量即当前元素插入到有序区中该位置后面。
重复执行2-4步,直到无序区为空,排序完成。
2. 直接插入排序的算法如下:
void InsertSort(RecType R[],int n)
{ int i, j; RecType tmp;
for (i=1;i=0 && R[j].key>tmp.key)
R[j+1]=tmp; //在j+1处插入R[i]
}
}
}
3.算法分析
直接插入排序的时间复杂度为O(n^2),因为在最坏情况下,每个元素都需要与已排序序列中的每个元素比较,即需要进行n-1次比较。同时,在每一轮比较中,需要将已排序序列中后面的元素依次向后移动,最坏情况下需要移动n-1次。因此,总的比较次数和移动次数都是n(n-1)/2,即时间复杂度为O(n^2)。
直接插入排序的空间复杂度为O(1),因为排序过程中只需要使用常量级别的额外空间,即只需要存储i,j,temp。不需要创建额外的数组或其他数据结构,所以空间复杂度为O(1)。
1.排序思路
折半插入排序是一种改进过的插入排序算法,它与直接插入排序的区别在于找插入位置时采用二分查找的方式,从而减少比较次数。具体描述如下:
将待排序序列第一个元素看作已排序序列,后面的元素看作未排序序列。
从未排序序列中取出一个元素,利用二分查找,在已排序序列中找到插入位置。
插入该元素,并将已排序序列中后面的元素依次向后移动,腾出插入位置。
重复执行步骤2和3,直到未排序序列中没有元素
2.折半插入排序算法如下:
void binaryInsertionSort(int arr[], int n)
{
int i, j, left, right, mid;
int key;
for (i = 1; i < n; i++) {
key = arr[i];
left = 0;
right = i - 1;
// 二分查找插入位置
while (left <= right) {
mid = (left + right) / 2;
if (key < arr[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// 将元素插入到正确位置
for (j = i - 1; j >= left; j--) {
arr[j+1] = arr[j];
}
arr[left] = key;
}
}
3.算法分析
半插入排序是一个在普通插入排序的基础上进行了优化的算法。它通过使用二分查找确定元素的正确位置,可以减少比较操作的次数,从而提高效率。
时间复杂度
折半插入排序的时间复杂度为O(n^2)。虽然二分查找能够减少比较操作的次数,但是每次插入元素时,仍需要将大于该元素的所有元素后移一个位置,这个操作需要O(n)的时间复杂度。因此,总的时间复杂度仍为O(n^2)。
空间复杂度
折半插入排序的空间复杂度为O(1),因为只需要使用常数级别的额外空间来存储一些临时变量。
稳定性
折半插入排序是一种稳定的排序算法。如果数组中存在相同的元素,在排序后,它们的相对顺序不会改变。
适用性
折半插入排序适用于数据规模较小的情况下,比如10万以下的数据量。当数据规模较大时,其效率很低,并且在最坏情况下,时间复杂度会退化到O(n^2)。
1.排序思路:
希尔排序是一种改进的插入排序算法,也称为“缩小增量排序”。它通过将待排序数组分成多个子序列进行插入排序,从而达到提高效率的目的。希尔排序的基本思路如下:
选择一个增量序列,通常取n/2、n/4、n/8...直到1。
根据选定的增量,将待排序数组分成若干个子序列,分别对每个子序列进行插入排序。
不断缩小增量,重复步骤2,直到增量为1时,完成最后一次插入排序,得到排好序的数组。
2.希尔排序算法如下:
void ShellSort(RecType R[],int n)
{ int i, j, d;
RecType tmp;
d=n/2; //增量置初值
while (d>0)
{ for (i=d;i=0 && tmp.key
3.算法分析
希尔排序的时间复杂度取决于其使用的增量序列。最坏情况下,当增量序列为1时,与插入排序相同,时间复杂度为O(n^2);而在最好情况下,当增量序列为h[k]=3*h[k-1]+1时,时间复杂度可以达到O(n^(3/2))。平均时间复杂度约为O(nlogn)。
空间复杂度:
希尔排序是原地排序算法,不需要额外的存储空间,因此空间复杂度为O(1)。
常见的交换排序方法:
(1)冒泡排序(或起泡排序)
(2)快速排序
1.排序思路
冒泡排序是一种简单的排序算法,它重复地遍历要排序的列表,每次比较相邻的两个元素,如果它们的顺序错误就交换它们。这样每一趟都会将最大(或最小)的元素“冒泡”到最后面,直到所有元素都排好序为止。
具体实现步骤如下:
从第一个元素开始,比较相邻的两个元素,如果前一个元素大于后一个元素,就交换它们的位置。
继续比较下一组相邻的元素,直到比较到最后一对元素。
重复以上步骤,每次遍历都能确定一个最大(或最小)的元素被“冒泡”到了最后面。
在每次遍历中,可以减少比较的次数,因为每次遍历都会把一个元素排好序,所以下一次遍历时只需要比较前面未排序的元素即可。
最终,当所有元素都排好序后,排序完成。
2.算法实现如下:
void BubbleSort(RecType R[],int n)
{ int i,j; RecType temp;
for (i=0;ii;j--) //比较找本趟最小关键字的元素
if (R[j].key
我们知道一旦某一趟比较时不出现元素交换,说明已排好序了,就可以结束本算法。比如(2,1,3,4,5,6)一旦1与2交换后就已经排序好了,而前面的算法仍然要执行好几趟,当它没有交换时说明已经排序好了,因此我们可以从此入手。设置一个标志exchange,有交换时令它为1,无交换令它为0,如果一旦exchange等于0便可以结束算法。代码实现如下:
void BubbleSort(RecType R[],int n)
{ int i,j; int exchange; RecType temp;
for (i=0;ii;j--)
if (R[j].key
3.算法分析
时间复杂度:
空间复杂度:
1.排序思路
快速排序是一种分治思想的排序算法,其基本思路如下:
具体实现时,一般采用双指针法来进行分区操作。首先将基准 赋值给left指针对应的元素,然后 right 指针从右往左扫描,找到第一个小于等于 pivot 的元素;接着 left 指针从左往右扫描,找到第一个大于 pivot 的元素;交换这两个元素。重复这个过程,直到 left 指针和 right 指针相遇,将 pivot 放回它最终的位置,即左子序列的最后一个元素或者右子序列的第一个元素。
假设有一个数组 arr = [5, 1, 9, 3, 7, 6]
快速排序的步骤如下:
按照上述步骤进行快速排序后,最终得到的有序数组为 [1, 3, 5, 6, 7, 9]。
2算法实现如下:
int partition(RecType R[],int s,int t) //一趟划分
{ int i=s,j=t;
RecType tmp=R[i]; //以R[i]为基准
while (ii && R[j].key>=tmp.key)
j--; //从右向左扫描,找一个小于tmp.key的R[j]
if(i
3.算法分析
快速排序的时间复杂度为O(nlogn),其中n为待排序元素的数量。空间复杂度则为O(logn)。
常见的选择排序方法:
(1)简单选择排序(或称直接选择排序)
(2)堆排序
1.排序思路:
简单选择排序的排序思路如下:
从待排序序列中,找到最小元素。
如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换位置。
从剩余的待排序元素中继续寻找最小元素,重复步骤2,直到所有元素已经排序完毕。
具体实现时,可以使用两层循环来实现。外层循环依次遍历待排序序列中的每一个元素,内层循环从当前元素的下一个位置开始遍历,找到最小元素后进行交换。
2.算法实现如下:
void SelectSort(RecType R[],int n)
{ int i,j,k;
for (i=0;i
3.算法分析
时间复杂度:O(n^2)
空间复杂度:O(1)
1.算法思路
堆排序是一种基于二叉堆数据结构的排序算法,它将未排序的元素依次插入堆中,然后取出最小(或最大)元素放在已排序的列表末尾,不断重复这个过程直到所有元素被排好序。具体思路如下:
将待排序数组转换成一个二叉堆,通常使用最小堆进行排序;
取出堆顶元素(即最小元素),与堆底元素交换位置;
对剩余的 n-1 个元素再次进行堆化操作,得到新的堆;
重复步骤 2 和 3 直到排序完成。
堆排序需要维护一个堆数据结构,可以使用数组实现。将数组看成完全二叉树,根节点为第 0 个元素,对于任意一个节点 i,他的左子节点为 2i+1,右子节点为 2i+2。堆排序主要包含两部分:构建堆和排序。
构建堆:
从最后一个非叶子节点开始,向前逐个处理每个节点,保证该节点所在的子树满足堆的性质。具体而言,就是通过向下递归交换节点,使得当前节点比其子节点小(或大,取决于堆的类型)。
排序:
每次取出堆顶元素,放在数组的最后一个位置,然后重新构建堆,再次取出堆顶元素,放在数组的倒数第二个位置,重复这个过程直到排序完成。
2.算法实现如下:
void sift(RecType R[],int low,int high) //调整堆的算法
{ int i=low,j=2*i; //R[j]是R[i]的左孩子
RecType tmp=R[i];
while (j<=high)
{
if (j=1;i--) //循环建立初始堆
sift(R,i,n);
for (i=n; i>=2; i--) //进行n-1次循环,完成推排序
{
swap(R[1],R[i]); //R[1]与R[i]交换
sift(R,1,i-1); //筛选R[1]结点,得到i-1个结点的堆
}
}
3.算法分析
堆排序的时间复杂度为 O(nlogn),其中 n 表示待排序的元素个数。堆排序中,建堆的时间复杂度为 O(n),每次取出堆顶元素后,需要进行一次堆化操作,时间复杂度为 O(logn),因此,总时间复杂度为 O(nlogn)。
堆排序的空间复杂度为 O(1),即不需要额外的存储空间,只需要在原数组上进行操作。但是,实际上,由于堆排序需要维护一个堆数据结构,这个堆可以使用数组来表示,因此,在实现时会占用一些额外的空间。但是,堆排序的空间复杂度仍然是 O(1),即与输入规模无关。
归并排序是多次将相邻两个或两个以上的有序表合并成一个新有序表的排序方法。
二路归并排序是多次将相邻两个的有序表合并成一个新有序表的排序方法,是最简单的归并排序。
1.排序思路
归并排序是一种分治算法,其基本思路是将待排序的序列分成若干个子序列,每个子序列都是有序的,然后再将子序列合并成更大的有序序列,直到最终只剩下一个有序序列。
具体实现步骤如下:
将待排序的序列不断地二分,直至每个子序列只有一个元素为止;
将相邻的子序列进行合并,得到新的有序序列;
不断重复第2步,直至只剩下一个有序序列,这个序列就是排好序的结果。
在合并两个有序序列时,可以使用双指针法。首先定义两个指针分别指向两个有序序列的起始位置,比较两个指针所指元素的大小,将小的放入新的有序序列中,并将该元素所在序列的指针向后移动一位;重复这个操作,直到某一个指针到达了序列的末尾,然后将另一个序列中剩余的元素全部放入新的有序序列中。
2.算法实现如下:
自低向上
void Merge(RecType R[],int low,int mid,int high)
{ RecType *R1;
int i=low,j=mid+1,k=0; //k是R1的下标,i、j分别为第1、2段的下标
R1=(RecType *)malloc((high-low+1)*sizeof(RecType));
while (i<=mid && j<=high)
if (R[i].key<=R[j].key) //将第1段中的元素放入R1中
{
R1[k]=R[i];
i++;k++;
}
else //将第2段中的元素放入R1中
{
R1[k]=R[j];
j++;k++;
}
while (i<=mid) //将第1段余下部分复制到R1
{
R1[k]=R[i];
i++;
k++;
}
while (j<=high) //将第2段余下部分复制到R1
{
R1[k]=R[j];
j++;k++;
}
for (k=0,i=low;i<=high;k++,i++) //将R1复制回R中
R[i]=R1[k];
free(R1);
}
void MergePass(RecType R[],int length,int n)//对整个排序序列进行一趟归并
{ int i;
for (i=0;i+2*length-1
3.算法分析
归并排序的时间复杂度为 O(nlogn),其中 n 表示待排序序列的长度。具体分析如下:
分解阶段:每次将待排序序列二分,直到每个子序列只剩一个元素,需要二分 logn 次。
合并阶段:每次合并两个有序子序列时需要比较大小,将元素放入新序列中,最坏情况需要遍历整个序列,因此时间复杂度为 O(n)。
由于每次合并时需要申请额外的空间存储结果序列,因此归并排序的空间复杂度为 O(n)。
1.排序思路:
基数排序是一种非比较排序算法,它将待排序的元素拆分成若干位,每一位可以看作是一次排序。基数排序按照低位先排序,然后收集;再按照高位排序,然后再收集;依此类推,直到最高位排序完成。
具体实现步骤如下:
找到待排序数组中最大的数,确定最大数的位数。
对每一位进行桶排序,以保证相同位数的元素在同一个桶里。
对每一位排序完后,按照顺序把所有桶中的元素依次取出来,组成新的序列。
重复第2和第3步,直到所有位都被排序完毕。
#define MAXE 20 //线性表中最多元素个数
#define MAXR 10 //基数的最大取值
#define MAXD 8 //关键字位数的最大取值
typedef struct node
{ char data[MAXD]; //元素的关键字定义的字符串
struct node *next;
} RecType1; //单链表中每个结点
2.算法实现如下:
void RadixSort(RecType1 *&p,int r,int d)
//p为待排序序列链表指针,r为基数,d为关键字位数
{ RecType1 *head[MAXR], *tail[MAXR], *t; //定义各链队的首尾指针
int i, j, k;
for (i=0;idata[i]-'0'; //找第k个链队
if (head[k]==NULL) //进行分配,即采用尾插法建立单链表
{
head[k]=p;
tail[k]=p;
}
else
{
tail[k]->next=p;
tail[k]=p;
}
p=p->next; //取下一个待排序的结点
}
p=NULL;
for (j=0;jnext=head[j];
t=tail[j];
}
}
t->next=NULL; //最后一个结点的next域置NULL
}
3.算法分析:
基数排序的时间复杂度为O(d*(n+k)),其中d表示最大数的位数,n表示数组大小,k表示每个桶的大小。
基数排序的优点是稳定性好,对于小范围数据排序效率高,适用于数据量不大的情况。缺点是需要大量的存储空间,如果数据范围过大则可能不适合使用。