排序概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序
排序的几种分类
常见的排序算法及主要思想:
- 插入排序:每次将一个元素插入到已经排好序的有序序列中,从前向后依次对所有元素进行插入排序。
- 选择排序:每次在待排序的数据元素中选出最小(或最大)的一个元素,然后将其与第一个元素交换位置。
- 冒泡排序:通过相邻元素之间的比较和交换位置的方式,使得每次比较后最大(或最小)的元素都会沉底。
- 希尔排序:是插入排序的一种更高效的改进版本。它利用插入排序在近乎有序的情况下的性能,通过预先部分排序将输入序列分割成几个子序列,分别进行插入排序。
- 归并排序:采用分治法(Divide and Conquer)的思想,将已有序的子序列合并,得到完全有序的序列。
- 快速排序:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
- 堆排序:利用堆这种数据结构来排序。通过构建大顶堆和调整堆的方式,每次都能找到当前序列的最大值。
以上是几种常见的排序算法,它们的时间复杂度和稳定性不同。排序的目的是使数据按某种顺序排列,是算法和数据结构中重要的基础概念。
排序过程与实现
1.冒泡排序
1.1 主要思想及过程
主要思想是通过不断比较相邻的元素并交换它们,使得较大(或较小)的元素逐渐“浮”到数组的一端。具体来说,冒泡排序的主要步骤如下:
- 从数组的第一个元素开始,依次比较相邻的两个元素,如果顺序不对则交换它们,使得较大(或较小)的元素向后移动。
- 继续对数组中的每一对相邻元素进行比较和交换,直到所有元素都被遍历一次。这样一次遍历之后,最大(或最小)的元素就会“冒泡”到数组的末尾。
- 重复以上步骤,每次遍历都会将当前未排序部分的最大(或最小)元素“冒泡”到相应位置,直到整个数组有序。
冒泡排序的时间复杂度为O(n2),其中n为数组的长度。尽管冒泡排序简单直观,但对于大规模数据集来说效率较低,通常不适用于大型数据排序。
下图是冒泡排序的过程动图。
1.2 代码实现
//冒泡
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n; j++)
{
for (int i = 0; i < n - j -1 ; i++)
{
if (a[i] > a[i + 1])
{
Swap(&a[i], &a[i + 1]);
}
}
}
}
运行结果
2. 选择排序
2.1 主要思想及过程
主要思想是在未排序部分中选择最小(或最大)的元素,然后将其放到已排序部分的末尾。
选择排序的主要步骤如下:
- 从数组中选择最小(或最大)的元素,将其与数组的第一个元素交换位置,此时第一个元素就是已排序部分的最小(或最大)元素。
- 在剩余未排序的部分中继续选择最小(或最大)的元素,将其与已排序部分的末尾元素交换位置,将其加入已排序部分。
- 重复以上步骤,每次选择出未排序部分的最小(或最大)元素并加入已排序部分,直到整个数组排序完成。
选择排序的时间复杂度也为O(n2),其中n为数组的长度。虽然选择排序比冒泡排序稍微高效一些,但仍然不适用于大规模数据排序。和冒泡排序一样,选择排序是一种简单但不够高效的排序算法。
动图如下:
2.2 代码实现
这里的代码思想相较上图有所改进,遍历一次同时记录未排序部分的最大与最小值,而后交换至未排序部分的首尾处。
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;//从首尾同时开始
while (begin < end)
{
int max_i = end, min_i = begin;//先假定max与min所在位置
for (int i = begin; i <= end; i++)//每一趟遍历都找到一个max与min
{//这里不能对begin与end做修改
if (a[i] < a[min_i])
{
min_i = i;//更新min的位置
}
if (a[i] > a[max_i])
{
max_i = i;//更新max的位置
}
}
Swap(&a[begin], &a[min_i]);//交换min与begin处的值
if (begin == max_i)//如果此时begin与max在同一位置,那么之前的交换就将max交换到了min处
{
max_i = min_i;//这种情况下更新max位置
}
Swap(&a[end], &a[max_i]);
--end;
++begin;
}
}
运行结果
3. 插入排序
3.1 主要思想及过程
主要思想是将未排序部分的元素逐个插入到已排序部分的合适位置,从而逐步构建有序序列。
插入排序的主要步骤如下:
1. 将数组视为两部分:已排序部分和未排序部分。初始时,已排序部分只包含数组的第一个元素,而未排序部分包含剩余的元素。
2. 从未排序部分取出第一个元素,将其插入到已排序部分的合适位置,使得插入后仍然保持有序。
3. 继续从未排序部分取出元素,逐个插入到已排序部分的合适位置,直到未排序部分为空,所有元素都被排序。插入排序的时间复杂度为O(n^2),最好情况下是O(n),其中n为数组的长度。插入排序在处理小型数据集或部分有序的数据时表现良好,因为它的内循环在有序部分中的操作次数较少。因此,插入排序通常用于对小型数据集进行排序,或作为其他排序算法的一部分。
动图如下:
3.2 代码实现
void InsertSort(int* a, int n)
{
for (int i = 0; i < n-1; i++)
{
int end = i;//已排序部分的最后一个
int tmp = a[end + 1];//未排序元素
while (end >= 0)
{
if (a[end] > tmp)//已排序部分的末位大于tmp
{
a[end + 1] = a[end];//元素后移
end--;//end左移
}
else//找到合适位置,退出循环
{
break;
}
}
a[end + 1] = tmp;//将tmp插入
}
}
运行结果
4. 希尔排序
4.1 主要思想及过程
希尔排序(Shell Sort)是一种改进的插入排序算法,也被称为“缩小增量排序”。希尔排序通过将数组分割成若干个子序列,对这些子序列分别进行插入排序,最后再对整个数组进行一次插入排序,从而实现对整个数组的排序。
希尔排序的主要步骤如下:
1. 选择一个增量序列,通常为n/2,n/4,n/8,...直到增量为1。
2. 对每个增量进行插入排序,即将数组分成若干个子序列,对每个子序列进行插入排序。
3. 逐渐减小增量,重复进行插入排序,直到增量为1。
4. 最后对整个数组进行一次插入排序。希尔排序的时间复杂度取决于增量序列的选择,一般情况下在O(n\log n)到O(n^2)之间。希尔排序相比于插入排序有更好的性能,尤其适用于中等规模的数据集。希尔排序是一种高效的排序算法,尽管不如快速排序或归并排序那样高效,但在某些情况下仍然是一个不错的选择。
动图如下:
4.2 代码实现
这里采取增量每次缩小三分之一的思想,最终的增量会变成1。
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap =gap/3+1;//gap每次缩小三倍,直到为1进行一次直接插入排序
for(int i=0;i<n-gap;i++)
{
int end = i;
int tmp = a[end + gap];//以gap为单位,序列内相隔gap的元素为一组进行插入排序
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];//切记gap
end-=gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
运行结果
5. 堆排序
5.1 主要思想及过程
堆排序(Heap Sort)是一种利用堆数据结构进行排序的算法,它利用了堆的性质来实现排序。堆是一种特殊的二叉树结构,分为最大堆和最小堆,其中最大堆满足父节点的值大于等于子节点的值,最小堆则相反。
堆排序的主要步骤如下:
1. 将待排序的数组构建成一个最大堆(或最小堆)。
2. 将堆顶元素(最大值或最小值)与堆的最后一个元素交换位置,然后对剩余的元素重新构建最大堆(或最小堆)。
3. 重复上述步骤,直到所有元素都被取出并排好序。堆排序的时间复杂度为O(n*log n),其中n为数组的长度。堆排序是一种原地排序算法,不需要额外的空间,但由于其涉及到堆的构建和调整,实现起来较为复杂。堆排序适用于大规模数据集的排序,效率较高。
5.2 代码实现
这里我们建堆的过程使用向上调整算法,排序时使用向下调整算法
void BigADjustUP(int* a, int child)//大堆的向上调整,从子节点向上调整
{
int parent = (child - 1) / 2;//寻找子节点的父亲
while (child >= 0)
{
if (a[child] > a[parent])//如果孩子大于父亲,
{
Swap(&a[child], &a[parent]);//交换父子的位置
child = parent;//更新孩子节点
parent = (parent - 1) / 2;//更新父亲节点
}
else
break;
}
}
void BigADjustDown(int* a, int size, int parent)//大堆的向下调整
{
int child = parent * 2 + 1;//找到父亲的孩子
while (child < size)//孩子在size范围内
{
if (child + 1 < size && a[child] < a[child + 1])//左孩子小于右孩子
{
child = child + 1;//更新最大的孩子位置
}
if (a[child] > a[parent])//孩子大于父亲
{
Swap(&a[child], &a[parent]);//交换
parent = child;//更新父亲与孩子的位置
child = parent * 2 + 1;
}
else
break;
}
}
void HeapSort(int* a, int n)
{
for (int i = 0; i < n; i++)
{
BigADjustUP(a,i);//建堆
}
//for (int i = 0; i < n; i++)
while (n > 0)//每次找到一个最大值放在最后面
{
Swap(&a[0], &a[n - 1]);//交换最后的节点与根节点位置
BigADjustDown(a, n - 1, 0);//向下调整
n--;//每次调整完最后一个节点不再参与排序
}
}
运行结果
6. 快速排序
快速排序有很多种方法,这里介绍四种:霍尔排序法、挖坑法、前后指针法、非递归法。 即前三种都是递归完成的。
6.1 霍尔法
6.1.1 主要思想及过程
霍尔方法(Hoare Partition Scheme)是快速排序算法中一种常用的分区方案,由Tony Hoare提出。霍尔方法通过选择一个基准值(key),将数组分为两部分,使得左边的元素都小于等于基准值,右边的元素都大于等于基准值,然后递归地对左右两部分进行排序。
霍尔方法的主要步骤如下:
1. 选择一个基准值(通常是数组的第一个元素)作为key。
2. 设置两个指针,一个指向数组的起始位置,一个指向数组的末尾。
3. 移动右指针,直到找到一个小于等于key的元素;移动左指针,直到找到一个大于等于key的元素;如果左指针小于右指针,则交换这两个元素。
4. 重复步骤3,直到左指针大于等于右指针。
5. 将key与左指针所指的元素交换位置,此时key左边的元素都小于等于key,右边的元素都大于等于key。
6. 递归地对左右两部分进行排序。
动图:
6.1.2 代码实现
int Getmidi(int* a, int begin, int end)//三数取中
{
int midi = (begin + end) / 2;
if (a[begin] < a[midi])
{
if (a[begin] > a[end])
return begin;
else if (a[midi] < a[end])
{
return midi;
}
else
return end;
}
else
{
if (a[begin] < a[end])
return begin;
else if (a[midi] > a[end])
{
return midi;
}
else
return end;
}
}
int HoareQsort(int* a, int begin, int end)
{
int mid_i = Getmidi(a, begin, end);//这里使用三数取中法来获取基准值,比之以首元素为基准值效率有所提升
Swap(&a[mid_i], &a[begin]);
int key_i = begin;//此时begin为三数的中间数
int left = begin, right = end;//定义左右指针
while (left < right)//两指针不能相遇
{
while (left < right && a[right] >= a[key_i])//右边先走,找小
{
right--;
}
while (left < right && a[left] <= a[key_i])//左边再走,找大
{
left++;
}
Swap(&a[left], &a[right]);//左右都停下,交换
}
Swap(&a[left], &a[key_i]);//此时左右指针相遇,交换指针与key_i处的值
return left;//返回此时的keyi,此时keyi左边都比他小,右边都比他大,即位置已经固定
}
void HoareQuickSort(int* a, int begin, int end)//霍尔方法的递归
{
if (begin >= end)//递归至每个区间只有一个元素,返回
return;
int key_i = HoareQsort(a, begin, end);//返回霍尔方法得出的固定位置
HoareQuickSort(a, begin, key_i - 1);//以此位置为分割,分别递归左右方的区间
HoareQuickSort(a, key_i + 1, end);
}
6.2 挖坑法
6.2.1 主要思想及过程
快速排序的挖坑法(Partition方法)是一种常用的快速排序算法,其主要思想是通过选取一个基准值(通常是数组的第一个元素),将数组分为两部分,一部分比基准值小,另一部分比基准值大,然后递归地对这两部分进行排序。
具体过程如下:
1. 选取一个基准值(通常是数组的第一个元素)作为比较标准。
2. 设置两个指针,一个指向数组的起始位置(一般是左指针),一个指向数组的末尾位置(一般是右指针)。
3. 从右边开始,找到第一个小于基准值的元素,将其填入左指针的位置,左指针向右移动。
4. 从左边开始,找到第一个大于基准值的元素,将其填入右指针的位置,右指针向左移动。
5. 重复步骤3和步骤4,直到左指针和右指针相遇。
6. 将基准值填入左指针和右指针相遇的位置,此时基准值左边的元素都小于基准值,基准值右边的元素都大于基准值。
7. 递归地对基准值左边的部分和右边的部分进行相同的操作,直到排序完成。通过不断地将数组分割为两部分并对每部分进行排序,最终可以得到一个有序的数组。挖坑法是快速排序中常用的一种分区方法,可以高效地对数组进行排序。
事实上挖坑法与霍尔方法思想上差不多,每趟排序都会确定一个基准值的最终位置,也就是左右指针相遇的位置。
6.2.2 代码实现
int Getmidi(int* a, int begin, int end)//三数取中
{
int midi = (begin + end) / 2;
if (a[begin] < a[midi])
{
if (a[begin] > a[end])
return begin;
else if (a[midi] < a[end])
{
return midi;
}
else
return end;
}
else
{
if (a[begin] < a[end])
return begin;
else if (a[midi] > a[end])
{
return midi;
}
else
return end;
}
}
int DigHoleQsort(int* a, int begin, int end)
{
int mid_i = Getmidi(a, begin, end);//三数取中
Swap(&a[mid_i], &a[begin]);//交换中间数至begin处
int hole_i = begin;//在begin处挖坑
int key = a[hole_i];//记录基准值
int left = begin, right = end;
while (left < right)//左右指针相遇即终止
{
while (left < right && a[right] >= key)//右边先走,找小
{
right--;
}
a[hole_i] = a[right];//找到小的填入坑内
hole_i = right;
while (left < right && a[left] <= key)//左边再走,找大
{
left++;
}
a[hole_i] = a[left];//找到大的填入坑内
hole_i = left;
}
a[hole_i] = key;//将基准值填入坑内
return hole_i;
}
void DigHoleQuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int key_i = DigHoleQsort(a, begin, end);
DigHoleQuickSort(a, begin, key_i - 1);
DigHoleQuickSort(a, key_i + 1, end);
}
6.3 前后指针法
6.3.1 主要思想及过程
前后指针法是快速排序的另一种常用的分区方法,也称为双指针法或荷兰国旗问题。
具体过程如下:
1. 选取一个基准值(通常是数组的第一个元素)作为比较标准。
2. 设置两个指针,一个指向数组的起始位置(一般是左指针),一个指向数组的末尾位置(一般是右指针)。
3. 左指针向右移动,直到找到一个大于等于基准值的元素。
4. 右指针向左移动,直到找到一个小于等于基准值的元素。
5. 如果左指针小于右指针,交换两个指针所指向的元素。
6. 重复步骤3和步骤4,直到左指针大于等于右指针。
7. 将基准值与右指针所指向的元素交换位置,此时基准值左边的元素都小于基准值,基准值右边的元素都大于基准值。
8. 递归地对基准值左边的部分和右边的部分进行相同的操作,直到排序完成。前后指针法通过左右指针的移动,实现了对数组的原地分区,可以高效地对数组进行排序。
6.3.2 代码实现
int Getmidi(int* a, int begin, int end)//三数取中
{
int midi = (begin + end) / 2;
if (a[begin] < a[midi])
{
if (a[begin] > a[end])
return begin;
else if (a[midi] < a[end])
{
return midi;
}
else
return end;
}
else
{
if (a[begin] < a[end])
return begin;
else if (a[midi] > a[end])
{
return midi;
}
else
return end;
}
}
int Prev_LastPointerQSort(int* a, int begin, int end)
{
//cur找小,找到就停止,与++prev交换,分区:0-prev(小于基准值),prev-cur(大于基准值),cur-end(未排序)
int mid_i = Getmidi(a, begin, end);//三数取中
Swap(&a[mid_i],&a[begin]);//交换中间数至begin处
int key_i = begin;//记录基准值下标
int prev = begin, cur = prev + 1;//定义前后指针
while (cur <= end)//前面指针越界即停止
{
if (a[cur] < a[key_i] && ++prev != cur)//cur处小于基准值,并且prev的下一个不等于cur
{
Swap(&a[cur], &a[prev]);//交换cur与prev的值
}
++cur;//cur++
}
Swap(&a[prev], &a[key_i]);//循环结束,交换基准值与prev处元素
key_i = prev;//此时基准值在prev处
return key_i;//返回已确定元素的位置
}
void Prev_LastPointerQuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int key_i = Prev_LastPointerQSort(a, begin, end);
Prev_LastPointerQuickSort(a, begin, key_i - 1);
Prev_LastPointerQuickSort(a, key_i + 1, end);
}
6.4 快排的非递归实现
6.4.1 主要思想及过程
非递归法是指在实现快速排序时,不使用递归调用的方法来进行分区和排序。一种常见的非递归快速排序方法是使用栈来模拟递归调用的过程,实现分区和排序。
具体过程如下:
1. 将数组的起始位置和结束位置入栈,表示整个数组需要排序。
2. 循环执行以下步骤,直到栈为空:
a. 出栈得到当前子数组的起始位置和结束位置。
b. 选取一个基准值(通常是数组的第一个元素)作为比较标准。
c. 使用前后指针法或挖坑法对当前子数组进行分区,将数组分为两部分。
d. 将分区后的左右子数组的起始位置和结束位置入栈,表示需要对这两部分进行排序。
3. 循环结束后,整个数组就被排序完成。通过使用栈来模拟递归调用的过程,非递归快速排序可以避免递归调用带来的额外开销,提高排序的效率。这种方法在实际应用中也是比较常见的一种实现方式。
6.4.2 代码实现
在c语言中,这里的栈是需要我们自己实现接口的。
void NonRecQsort(int* a, int begin, int end)
{
Stack s;
StackInit(&s);
StackPush(&s, end);
StackPush(&s, begin);//区间入栈
while (StackEmpty(&s))//直到栈为空,所有区间出栈完成
{
int left = StackTop(&s);//区间出栈
StackPop(&s);
int right = StackTop(&s);
StackPop(&s);
int keyi = Prev_LastPointerQSort(a, left, right);//对区间进行一次排序,取keyi(左边的都比他小,右边的都比他大)
if (left < keyi - 1)//左区间元素个数大于一
{
StackPush(&s, keyi - 1);
StackPush(&s, left);
}
if (right > keyi + 1)//右区间元素个数大于1
{
StackPush(&s, right);
StackPush(&s, keyi + 1);
}
}
StackDestroy(&s);
}
各大排序效率比较
这里是100000个随机数的排序
void Test01()
{
srand(time(0));
int N = 100000;
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);
int* a3 = (int*)malloc(sizeof(int) * N);
int* a4 = (int*)malloc(sizeof(int) * N);
int* a5 = (int*)malloc(sizeof(int) * N);
int* a6 = (int*)malloc(sizeof(int) * N);
int* a7 = (int*)malloc(sizeof(int) * N);
int* a8 = (int*)malloc(sizeof(int) * N);
int* a9 = (int*)malloc(sizeof(int) * N);
int* a10 = (int*)malloc(sizeof(int) * N);
for (int i = 0; i < N; i++)
{
a1[i] = rand() + i;
a2[i] = a1[i];
a3[i] = a2[i];
a4[i] = a3[i];
a5[i] = a4[i];
a6[i] = a5[i];
a7[i] = a6[i];
a8[i] = a7[i];
}
int begin1 = clock();
BubbleSort(a1, N);
int end1 = clock();
printf("BubbleSort() : %d\n", end1-begin1);
int begin2 = clock();
InsertSort(a2, N);
int end2 = clock();
printf("InsertSort : %d\n", end2 - begin2);
int begin3 = clock();
ShellSort(a3, N);
int end3 = clock();
printf("ShellSort : %d\n", end3 - begin3);
int begin4 = clock();
SelectSort(a4, N);
int end4 = clock();
printf("SelectSort : %d\n", end4 - begin4);
int begin5 = clock();
HoareQuickSort(a5, 0,N-1);
int end5 = clock();
printf("HoareQuickSort : %d\n", end5 - begin5);
int begin6 = clock();
NonRecQsort(a6, 0, N - 1);
int end6 = clock();
printf("NonRecQsort : %d\n", end6 - begin6);
int begin7 = clock();
DigHoleQuickSort(a7, 0, N - 1);
int end7 = clock();
printf("DigHoleQuickSort : %d\n", end7 - begin7);
int begin8 = clock();
Prev_LastPointerQuickSort(a8, 0, N - 1);
int end8 = clock();
printf("Prev_LastPointerQuickSort : %d\n", end8 - begin8);
int begin9 = clock();
HeapSort(a9,N);
int end9 = clock();
printf("HeapSort : %d\n", end9 - begin9);
}