Wordpress python版上海seo优化外包公司
排序
1. 排序相关概念
将一个数据元素(或记录)的任意序列,重新排列成一个按关键字有序(递增/递减)的序列称为排序
设n个记录的序列为{R1,R2,…,Rn},其相应关键字序列为{K1,K2,…,Kn},这些关键字相互之间可以进行比较,即在它们之间存在着一种排序P1,P2,…,Pn,使其相应的关键字满足递增(升序),或递减(降序)的关系。
内排序
- 在排序过程中,所有排序记录调到内存中进行的排序,整个排序过程不需要访问外存
- 内排序是排序的基础
- 内存速度快,比较过程主导性能,内排序效率主要用比较次数和移动次数来衡量
外排序
- 排序过程中需对外存进行访问的排序
- 参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成
- 由于读写延迟是外存性能相关的主要问题,所以,外排序主要用读/写外存的次数作为衡量效率的指标
排序方法的稳定性
-
稳定的排序方法是若待排序的序列中,存在多个具有相同关键字的记录,经过排序,这些记录的相对次序保持不变,则称该算法是稳定的
原始序列:( 5, 3, 7, 2, 8, 10, 4, 7 )
排序结果:( 2, 3 ,4, 6, 7, 7, 8, 10 ) 是稳定的
排序结果:( 2, 3 ,4, 6, 7, 7, 8, 10 ) 是不稳定的
2. 插入排序
2.1 直接插入排序
插入排序类似于整理扑克牌。
以下图为例:
void InsertSort(int *a, int n)
{for (int i = 1; i < n; i++){int end = i - 1;int temp = a[i];while (end >= 0){if (temp < a[end]){a[end + 1] = a[end];end--;}else{break;}}a[end + 1] = temp;}
}
性能分析如下:
空间复杂度: O ( 1 ) O(1) O(1)
时间复杂度: O ( n 2 ) O(n^2) O(n2)
稳定性:因为每次插入元素时总是从后往前先比较再移动,所以不会出现相同元素相对位置发生变化的情况,即直接插入排序是一个稳定的排序算法。
2.2 折半插入排序
折半插入排序就是使用前面提到的折半查找法,来找到需要插入元素的位置,原理很简单。确定插入位置后,再统一向后挪动元素。
void BInsertSort(SqList &L)
{// 对顺序表L做折半插入排序int i, j, low, high, m;for (i = 2; i <= L.length; ++i){L.r[0] = L.r[i]; // 将待插入的记录暂存到监视哨中low = 1;high = i - 1; // 置查找区间初值while (low <= high) //折半查找法{ // 在r[low..high]中折半查找插入的位置m = (low + high) / 2; // 折半if (L.r[0].key < L.r[m].key)high = m - 1; // 插入点在前一子表elselow = m + 1; // 插入点在后一子表} // whilefor (j = i - 1; j >= high + 1; --j)L.r[j + 1] = L.r[j]; // 记录后移L.r[high + 1] = L.r[0]; // 将r[0]即原r[i],插入到正确位置} // for
}
从上述算法中,不难看出折半插入排序仅减少了比较元素的次数,时间复杂度约为 O ( n l o g n ) O(nlogn) O(nlogn)该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数 n n n;而元素的移动次数并未改变它依赖于待排序表的初始状态。因此,折半插入排序的时间复杂度仍为 O ( n 2 ) O(n^2) O(n2),但对于数据量不很大的排序表,折半插入排序往往能表现出很好的性能。折半插入排序是一种稳定的排序算法。折半插入排序仅适用于顺序存储的线性表,比如数组等。
2.3 希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
如下图所示,第一趟增量取5,第二趟取增量取3,第三趟增量为1。
代码如下:
// 教材写法
void ShellInsert(SqList &L, int dk)
{// 对顺序表L做一趟增量是dk的希尔插入排序int i, j;for (i = dk + 1; i <= L.length; ++i)if (L.r[i].key < L.r[i - dk].key){ // 需将L.r[i]插入有序增量子表L.r[0] = L.r[i]; // 暂存在L.r[0]for (j = i - dk; j > 0 && L.r[0].key < L.r[j].key; j -= dk)L.r[j + dk] = L.r[j]; // 记录后移,直到找到插入位置L.r[j + dk] = L.r[0]; // 将r[0]即原r[i],插入到正确位置} // for
}
// ShellInsert
void ShellSort(SqList &L, int dt[], int t)
{// 按增量序列dt[0..t-1]对顺序表L作t趟希尔排序int k;for (k = 0; k < t; ++k)ShellInsert(L, dt[k]); // 一趟增量为dt[t]的希尔插入排序
} // ShellSor
我更推荐下面这种写法,直观,便于理解。
template <class T>
void ShellSort(T *a, int n, int gap)
{while (gap > 1){gap = gap / 3 + 1;for (int i = 0; i < n - gap; i++){int end = i;T temp = a[end + gap];while (end >= 0){if (temp < a[end]){a[end + gap] = a[end];end -= gap;}else{break;}}a[end + gap] = temp;}}
}
性能分析如下:
空间复杂度: O ( 1 ) O(1) O(1)
时间复杂度:希尔排序的时间复杂度是尚未解决的难题,没有具体的值,以教材说法为主,主要与gap的值有关。
稳定性:不稳定,划分不同区间会改变相对次序。
3. 交换排序
3.1 冒泡排序
冒泡排序就不用多说了,典中典,刚开始学习C语言就接触到的排序算法。
template <class T>
void BubbleSort(T *a, int n)
{for (int i = 0; i < n - 1; i++){bool flag = false;for (int j = 0; j < n - i - 1; j++)//为什么是n-i-1呢,因为第n-i-1个元素是已经排好的{if (a[j] > a[j + 1]){std::swap(a[j], a[j + 1]);flag = true;}}if (!flag)break;}
}
第i趟 | 第i躺排好第几大的值 | 第i趟排好的的第几大的值所在下标 |
---|---|---|
1 | 1 | n-1 |
2 | 2 | n-2 |
3 | 3 | n-3 |
… | … | … |
n-1 | n-1 | 1 |
n | n | 0 |
相当于就是每次从前往后挪动,都把最大的挪到后面了。以下图为例。
性能分析如下:
空间复杂度: O ( 1 ) O(1) O(1)
时间复杂度: O ( n 2 ) O(n^2) O(n2)
稳定性:稳定
3.2 快速排序
快速排序算是排序算法中的重点和难点了,希尔排序是插入排序的优化,那么快速排序就是冒泡排序的优化。事实上,不论是C++STL、Java SDK、.NETFrameWorkSDK等开发工具包中的源代码中都能找到它的某种实现版本。
快速排序是一种分治算法,
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
int PartSort(int *a,int left,int right)
{// 以左边为基准,要先走右找小,再走左找大int key=left;while(left<right){while(left<right&&a[right]>=a[key]){right--;}while(left<right&&a[left]<=a[key]){left++;}std::swap(a[left],a[right]);}std::swap(a[left],a[key]);return left;
}
// 排序范围[left,right)
void QuickSort(int *a,int left,int right)
{if(left>=right)return ;int key=PartSort(a,left,right);QuickSort(a,left,key-1);QuickSort(a,key+1,right);
}
int PartSort(int *a,int left,int right)
{// 以左边为基准,要先走左找大,再走右找小int key=right;while(left<right){while(left<right&&a[left]<=a[key]){left++;}while(left<right&&a[right]>=a[key]){right--;}std::swap(a[left],a[right]);}std::swap(a[left],a[key]);return left;
}
// 排序范围[left,right)
void QuickSort(int *a,int left,int right)
{if(left>=right)return ;int key=PartSort(a,left,right);QuickSort(a,left,key-1);QuickSort(a,key+1,right);
}
性能分析如下:
空间复杂度:由于利用了递归,需要使用栈,最好情况下为 O ( l o g 2 n ) O(log_2n) O(log2n),最坏情况下为 O ( n ) O(n) O(n),平均情况下为
时间复杂度:快速排序是一种高效的排序算法,其核心思想基于分治法。在最理想的情况下,即每次划分都能将数组分为两个几乎相等的子序列时,快速排序的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。
然而,在最坏的情况下,对逆序和有序的序列使用快速排序,即每次选择的pivot(基准值)都是最大或最小元素,导致每次只能排除一个元素,剩下的元素还需要继续排序,这样会形成类似冒泡排序的情况,需要进行大约n次的划分,每次划分需要遍历几乎所有元素,因此时间复杂度退化为 O ( n 2 ) O(n^2) O(n2)。
稳定性:在划分算法中,若右端区间有两个关键字相同,且均小于基准值的记录,则在交换到左端区间后,它们的相对位置会发生变化,即快速排序是一种不稳定的排序算法。例如,表L={3,2,2},经过一趟排序后L={2,2,3},最终排序序列也是L={2,2.3},显然,2与2的相对次序已发生了变化。
适用性:仅使用与顺序存储的线性表
快速排序可以进行优化,如三数取中。
template <class T>
int minIndex(T *a, int left, int right) // 快速排序三数取中优化法
{int mid = (left + right) / 2;if (a[left] < a[right]){if (a[mid] < a[left]){return left;}else if (a[mid] > a[right]){return right;}else{return mid;}}else{if (a[mid] > a[left]){return left;}else if (a[mid] < a[right]){return right;}else{return mid;}}
}
4. 选择排序
4.1 简单选择排序
简单选择排序就是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的 数据元素排完 。原理简单,不过多说了。
void SelectSort(int *a, int n)
{for(int i=0;i<n;i++){int minIndex=i;for(int j=i;j<n;j++){if(a[minIndex]>a[j]){minIndex=j;}}if(minIndex!=i)std::swap(a[minIndex],a[i]);}
}
性能分析如下:
空间复杂度: O ( 1 ) O(1) O(1)
时间复杂度: O ( n 2 ) O(n^2) O(n2)
稳定性:在第i趟找到最小元素后,和第i个元素交换,可能会导致第i个元素与含有相同关键字的元素的相对位置发生改变。例如,表L={2,2,1},经过一趟排序后L={1,2,2},最终排序序列也是L={1,2.2},显然,2与2的相对次序已发生变化。因此,简单选择排序是一种不稳定的排序算法。
4.2 堆排序
堆排序就是使用堆这种数据结构来进行排序,注意一点,排升序建大堆,排降序建小堆。
每次建完堆后,将堆顶元素与最后一个元素交换,堆顶元素是最大值,然后在此重新向下调整建堆,要注意建堆的范围。
template <class T>
void AdjustDown(T *a, int parent, int n)
{int child = 2 * parent + 1;while (child < n){if (child + 1 < n && a[child + 1] < a[child])child++;if (a[child] < a[parent]) // 小则交换,构建小根堆{std::swap(a[child], a[parent]);parent = child;child = 2 * parent + 1;}else{ // 没有交换则不需要交换break;}}
}template <class T>
void HeapSort(T *a, int n)
{for (int i = n - 1 - 1 / 2; i >= 0; i--)AdjustDown(a, i, n);for (int i = n - 1; i > 0; i--){std::swap(a[0], a[i]);AdjustDown(a, 0, i);}
}
性能分析如下:
空间复杂度: O ( 1 ) O(1) O(1)
时间复杂度:向下调整建堆的时间复杂度为 O ( n ) O(n) O(n),证明过程见树那一章节,每次调整的时间复杂度为 O ( h ) O(h) O(h),故综合时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。
稳定性:堆排序可能把有相同关键字元素排到前面,故堆排序不稳定。
5. 归并排序、基数排序、计数排序
5.1 归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
- 把原始需要排序的序列分成两个或多个子序列
- 把每个子序列排好序
- 把多个排好序的子序列合并称一个序列,即为结果
void _Merge(int *a, int left, int right, int *tmp)
{if (left >= right)return;int mid = (left) + (right - left) / 2;_Merge(a, left, mid, tmp);_Merge(a, mid + 1, right, tmp);int begin1 = left, end1 = mid;int begin2 = mid + 1, end2 = right;int i = left;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2])tmp[i++] = a[begin1++];elsetmp[i++] = a[begin2++];}//当遍历完其中一个区间,将另一个区间剩余的数据直接放到tmp的后面while (begin1 <= end1)tmp[i++] = a[begin1++];while (begin2 <= end2)tmp[i++] = a[begin2++];// int j=0;for (int j = left; j <= right; j++)a[j] = tmp[j];
}
void MergeSort(int *a, int n)
{int *tmp = (int *)malloc(sizeof(int) * n);_Merge(a, 0, n - 1, tmp);free(tmp);
}
5.2 基数排序
对于数字型或字符型的单关键字,可以看成是由多个数位或多个字符构成的多关键字,采用多关键字排序,称作基数排序。
为实现多关键字排序,通常有两种方法:第一种是最高位优先(MSD)法,按关键字位权重递减依次逐层划分成若干更小的子序列,最后将所有子序列依次连接成一个有序序列;第二种是最低位优先(LSD)法,按关键字位权重递增依次进行排序,最后形成一个有序序列。
我们以研究生成绩排序为例,按照分数高低排序,分数相同按专业课排序。
#include <iostream>
#include <vector>
#include <queue>
#include <string>
#include <algorithm>
#include <iomanip>
#include <ctime>
#include "LinkQueue.hpp" //自己在实验2写的链栈// 基数排序
#define K 3 // 关键字个数 百位、十位、个位
#define Radix 10struct Student
{int stuid; // 考生号std::string name; // 姓名int totalScore; // 总成绩int majorScore; // 专业课Student() {}// 比较思路如下:// 进行两次分发回收,第一次以专业课,这样的话总分排序时,专业课高的一定在前面Student(int id, const std::string n, int t, int m) : stuid(id), name(n), totalScore(t), majorScore(m){}
};LinkQueue<Student> Q[Radix];
// 基数排序的实现
// 即使用队列将每一次分发的进行存储,每一次分发按照高位到低位进行
int GetKey(int value, int k)
{int key = 0;while (k >= 0){key = value % 10;value /= 10;k--;}return key;
}
void DistributeMajor(std::vector<Student> &a, int left, int right, int k)
{for (int i = left; i < right; i++){int key = GetKey(a[i].majorScore, k);Q[key].push(a[i]);}
}
void DistributeTotal(std::vector<Student> &a, int left, int right, int k)
{for (int i = left; i < right; i++){int key = GetKey(a[i].totalScore, k);Q[key].push(a[i]);}
}
void Collect(std::vector<Student> &a)
{int k = 0;for (int i = 0; i < Radix; i++){while (!Q[i].empty()){a[k++] = Q[i].front();Q[i].pop();}}
}void RadixSort(std::vector<Student> &a, int left, int right)
{for (int i = 0; i < K; i++){DistributeMajor(a, left, right, i);Collect(a);}for (int i = 0; i < K; i++){DistributeTotal(a, left, right, i);Collect(a);}
}int main()
{std::vector<Student> students = {{20240001, "Brian", 338, 82},{20240002, "Queen", 384, 82},{20240003, "Xiomara", 330, 94},{20240004, "Gwen", 347, 74},{20240005, "Ryan", 347, 75},{20240006, "Holly", 325, 92},{20240007, "Damon", 399, 88},{20240008, "Quinn", 440, 120},{20240009, "Yvette", 306, 94},{20240010, "Steve", 306, 79},{20240011, "Mona", 380, 73}};RadixSort(students, 0, students.size());std::cout<< "考生号 "<< "姓名 "<< "总分 "<< "专业课分数" << std::endl;// 打印学生数据for (int i = students.size() - 1; i >= 0; i--){std::cout << std::left<< std::setw(12) << students[i].stuid<< std::setw(10) << students[i].name<< std::setw(10) << students[i].totalScore<< std::setw(12) << students[i].majorScore << std::endl;}return 0;
}
算法 | 平均时间复杂度 | 最坏时间复杂度 | 额外空间复杂度 | 稳定性 | 说明 |
---|---|---|---|---|---|
简单选择排序 | O(N2) | O(N2) | O(1) | 不稳定 | |
冒泡排序 | O(N2) | O(N2) | O(1) | 稳定 | |
直接插入排序 | O(N2) | O(N2) | O(1) | 稳定 | |
快速排序 | O(NlogN) | O(N2) | O(logN) | 不稳定 | |
归并排序 | O(NlogN) | O(NlogN) | O(N) | 稳定 | |
堆排序 | O(NlogN) | O(NlogN) | O(1) | 不稳定 | |
希尔排序 | O(Nd) | O(N2) | O(1) | 不稳定 | 依赖增量序列d |
基数排序 | O(d(N+rd)) | O(d(N+rd)) | O(N+rd) | 稳定 | d是趟数;rd是基数 |