排序概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,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];//切记gapend-=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;//更新父亲节点}elsebreak;}
}
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;}elsebreak;}
}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;}elsereturn end;}else{if (a[begin] < a[end])return begin;else if (a[midi] > a[end]){return midi;}elsereturn 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;}elsereturn end;}else{if (a[begin] < a[end])return begin;else if (a[midi] > a[end]){return midi;}elsereturn 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;}elsereturn end;}else{if (a[begin] < a[end])return begin;else if (a[midi] > a[end]){return midi;}elsereturn 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);
}