若待排序表中有两个元素$R_i$和$R_j$,其对应的关键字相同即$key_i=key_j$,且在排序前$R_i$在$R_j$的前面,若使用某一排序算法排序后,$R_i$仍然在$R_j$的前面,则称这个排序算法是稳定的,否则称排序算法是不稳定的。算法是否具有稳定性并不能衡量一个算法的优劣,它主要是对算法的性质进行描述。如果待排序表中的关键字不允许重复,则排序结果是唯一的,那么选择排序算法时的稳定与否就无关紧要。
在排序过程中,根据数据元素是否完全在内存中,可将排序算法分为两类:
1、内部排序,是指在排序期间元素全部存放在内存中的排序;
2、外部排序,是指在排序期间元素无法全部同时存放在内存中,必须在排序的过程中根据要求不断地在内、外存之间移动的排序。
一般情况下,内部排序算法在执行过程中都要进行两种操作:比较和移动。通过比较两个关键字的大小,确定对应元素的前后关系,然后通过移动元素以达到有序。当然,并非所有的内部排序算法都要基于比较操作,如基数排序就不基于比较。
每种排序算法都有各自的优缺点,适合在不同的环境下使用,就其全面性能而言,很难提出一种被认为是最好的算法。
插入排序
直接插入排序算法适用于顺序存储和链式存储的线性表。
//直接插入排序
void InsertSort(int A[], int n){
int i, j, temp;
for(i = 1; i < n; i++){
temp = A[i];
j = i - 1;
while(j >= 0 && temp < A[j]){
A[j + 1] = A[j];
j--;
}
A[j + 1] = temp;
}
}
直接插入排序算法中,每趟插入的过程中都进行了两项工作:1、从前面的有序子表中查找出待插入元素应该被插入的位置;2、给插入位置腾出空间,将待插入元素复制到表中的插入位置。在直接插入排序算法中,总是边比较边移动元素。下面将比较和移动操作分离,即先折半查找出元素的待插入位置,然后统一地移动待插入位置之后的所有元素。当排序表为顺序表时,可以对直接插入排序算法做如下改进:由于是顺序存储的线性表,所以查找有序子表时可以用折半查找来实现。确定待插入位置后,就可统一地向后移动元素。
折半插入排序仅减少了比较元素的次数,约为$O(n\cdot log_2n)$,该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数$n$;而元素的移动次数并未改变,它依赖于待排序表的初始状态。因此,折半插入排序的时间复杂度仍为$O(n^2)$,但对于数据量不很大的排序表,折半插入排序往往能表现出很好的性能。折半插入排序是一种稳定的排序方法。
//希尔排序
void shellSort(int a[], int n){
int i, j, gap; // gap 为步长,每次减为原来的一半。
for (gap = n / 2; gap > 0; gap /= 2){
// 共 gap 个组,对每一组都执行直接插入排序
for (i = 0; i < gap; i++){
for (j = i + gap; j < n; j += gap){
// 如果 a[j] < a[j-gap],则寻找 a[j]位置,并将后面数据的位置都后移。
if (a[j] < a[j - gap]){
int tmp = a[j];
int k = j - gap;
while (k >= 0 && a[k] > tmp){
k -= gap;
}
a[k + gap] = tmp;
}
}
}
}
}
交换排序
冒泡排序的基本思想是:从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即$A[i-1]>A[i]$),则交换它们,直到序列比较完。这被称为第一趟冒泡,结果是将最小的元素交换到待排序列的第一个位置(或将最大的元素交换到待排序列的最后一个位置),关键字最小的元素如气泡一般逐渐往上“漂浮”直至“水面”(或关键字最大的元素如石头一般下沉至水底)。下一趟冒泡时,前一趟确定的最小元素不再参与比较,每趟冒泡的结果是把序列中的最小元素(或最大元素)放到了序列的最终位置……这样最多做$n-1$趟冒泡就能把所有元素排好序。
//冒泡排序
void BubbleSort(int A[],int n){
for(int i = 0; i < n - 1; i++){
bool flag = false;
for(int j = n - 1; j > i; j--){
if(A[j - 1] > A[j]){
swap(A[j - 1],A[j]);
flag = true;
}
}
if(flag == false){
return;
}
}
}
//快速排序
void quick_sort(vector<int>& nums, int l, int r){
if (l >= r){
return;
}
int i = l - 1,j = r + 1, x = nums[i + j >> 1];
while(i < j){
do{
i++;
}while(nums[i] < x);
do{
j--;
}while(nums[j] > x);
if(i < j){
swap(nums[i], nums[j]);
}
}
quick_sort(nums, l, j),quick_sort(nums, j + 1, r);
}
选择排序
//选择排序
void SelectSort(int A[],int n){
for(i = 0; i < n - 1; i++){
min = i;
for(j = i + 1; j < n; j++){
if(A[j] < A[min]){
min = j;
}
}
if(min != i){
swap(A[i], A[min]);
}
}
}
void DownAdjust(int low, vector<int>& heap){
// low 为代调整节点
int i = low, j = i * 2;
while(j <= heap.size()){
if(j + 1 <= heap.size() && heap[j + 1] > heap[j]){
j = j + 1;
}
if(heap[j] > heap[i]){
swap(heap[j], heap[i]);
i = j;
j = i * 2;
}else{
break;
}
}
}
void CreateHeap(vector<int>& heap){
for(int i = n / 2; i >= 1; i--){
DownAdjust(i, heap);
}
}
归并排序
数组自顶向下版:
//归并排序
void merge_sort(int q[], int tmp[], int l, int r){
if(l >= r){
return;
}
int mid = l + r >> 1
merge_sort(q, tmp, l, mid);
merge_sort(q, tmp, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while(i <= mid && j <= r){
if(q[i] <= q[j]){
tmp[k++] = q[i++];
}else{
tmp[k++] = q[j++];
}
}
while(i <= mid){
tmp[k++] = q[i++];
}
while(j <= r){
tmp[k++] = q[j++];
}
for(i = l, j = 0; i <= r; i++, j++){
q[i] = tmp[j];
}
}
链表自顶向下版:
class Solution {
public:
//找到中间节点:two pointers
ListNode* find(ListNode* head){
ListNode* slow = new ListNode(0, head);
ListNode* fast = slow;
while(fast != nullptr && fast -> next != nullptr){
slow = slow -> next;
fast = fast -> next -> next;
}
return slow;
}
//合并子链表
ListNode* merge(ListNode* head1, ListNode* head2){
ListNode* pre = new ListNode();
ListNode* ret = pre;
while(head1 != nullptr && head2 != nullptr){
if(head1 -> val <= head2 -> val){
pre -> next = head1;
head1 = head1 -> next;
}else{
pre -> next = head2;
head2 = head2 -> next;
}
pre = pre -> next;
}
pre -> next = head1 != nullptr ? head1 : head2;
return ret -> next;
}
//递归调用自顶向下
ListNode* sortList(ListNode* head) {
if(head == nullptr){
return nullptr;
}
if(head -> next == nullptr){
return head;
}
ListNode* mid = find(head);
ListNode* head2 = mid -> next;
mid -> next = nullptr;
return merge(sortList(head), sortList(head2));
}
};
基数排序
内部排序总结
对排序算法的比较和应用应考虑以下情况:
1、选取排序方法需要考虑的因素:
①待排序的元素数目$n$。
②元素本身信息量的大小。
③关键字的结构及其分布情况。
④稳定性的要求。
⑤语言工具的条件,存储结构及辅助空间的大小等。
2、排序算法小结:
①若$n$较小,可采用直接插入排序或简单选择排序。由于直接插入排序所需的记录移动次数较简单选择排序的多,因而当记录本身信息量较大时,用简单选择排序较好。
②若文件的初始状态已按关键字基本有序,则选用直接插入或冒泡排序为宜。
③若$n$较大,则应采用时间复杂度为$O(nlogn)$的排序方法:快速排序、堆排序或归并排序。快速排序被认为是目前基于比较的内部排序方法中最好的方法,当待排序的关键字随机分布时,快速排序的平均时间最短。堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况,这两种排序都是不稳定的。若要求排序稳定且时间复杂度为$O(nlogn)$,则可选用归并排序。
④在基于比较的排序方法中,每次比较两个关键字的大小之后,仅出现两种可能的转移,因此可以用一棵二叉树来描述比较判定过程,由此可以证明:当文件的$n$个关键字随机分布时,任何借助于“比较”的排序算法,都至少需要$O(nlogn)$的时间。
⑤若$n$很大,记录的关键字位数较少且可以分解时,采用基数排序较好。
⑥当记录本身信息量较大时,为避免耗费大量时间移动记录,可用链表作为存储结构,此时仅修改相关结点的指针即可。



