数据结构系列7——排序
排序
概念
- 算法的稳定性:若待排序表中有两个元素\(R_i\)和\(R_j\),其对应的关键字相同即\(key_i = key_j\),其在排序前\(R_i\)就在\(R_j\)前面,若使用某一排序算法,在排序后\(R_i\)仍在\(R_j\)前面,那么称这一算法是稳定的
稳定性即指排序算法不破坏序列中原有的有序部分
- 并非所有排序都基于比较,例如基数排序
- 对任意关键字的比较排序,最少情况比较\(\lceil log_2(n!) \rceil\)
插入排序
- 直接插入排序
- 适合基本有序的序列
void insertSort(vector<int>& a) {
int n = a.size();
for (int i = 1; i < n; i++) {
int temp = a[i], j = i;
while (j > 0 && a[j - 1] > temp) {
a[j] = a[j - 1];
j--;
}
a[j] = temp;
}
}

- 折半插入排序
- 仅减少了比较元素的次数,时间复杂度仍是\(O(n^2)\)
void insertSort(vector<int>& a) {
int i, j, low, high, mid, temp, n = a.size();
for (i = 1; i < n; i++) {
temp = a[i];
low = 0; high = i - 1;
while (low <= high) {
mid = (low + high) / 2;
if (a[mid] > temp) high = mid - 1;
else low = mid + 1;
}
for (j = i - 1; j >= high + 1; j--) {
a[j + 1] = a[j];
}
a[high + 1] = temp;
}
}
- 希尔排序
- 分成若干个子表,步长为\(d_1 = n / 2, d_{i + 1} = \lfloor d_i / 2 \rfloor\),把相隔\(d_i\)的记录组成一个子表
- 计算子表时注意,若间隔为n,中间隔了n - 1个元素
void shellSort(vector<int>& a) {
int n = a.size();
for (int dk = n / 2; dk >= 1; dk = dk / 2) {
for (int i = dk; i < n; i++) { // 该子表的元素0, dk, 2dk, ...,因此类比插入排序,dk为第二个位置
if (a[i] < a[i - dk]) {
int j, temp = a[i];
for (j = i - dk; j >= 0 && temp < a[j]; j -= dk) { //寻找合适的插入位置
a[j + dk] = a[j]; // 插入位置后的元素依次后移
}
a[j + dk] = temp;
}
}
}
}

交换排序
- 冒泡排序
- 每一趟都会将一个元素放置到最终的位置上
void bubbleSort(vector<int>& a) {
int n = a.size();
for (int i = 1; i < n; i++) {
for (int j = 0; j < n - i; j++) {
// 每次比较维护着当前遇到的最大元素,因此每一次冒泡都可能改变序列
if (a[j] > a[j + 1]) swap(a[j], a[j + 1]);
}
}
}

- 快速排序
- 基于分治,每一趟都会将一个元素放置到最终位置,但不产生有序子序列,调整后左边的元素均不超过该元素,右边的元素均大于该元素
- 基于递归,平均情况下栈的深度\((log_2n)\),最坏情况下栈的深度\(O(n)\)
- 序列基本有序或基本逆序时,得到最坏的时间复杂度\(O(n^2)\)(基本有序时,递归树最不平衡)
- 最理想情况下,Partition最平衡划分,两个子问题的大小都不大于\(n / 2\)(递归划分的左右子树基本平衡),此时时间复杂度\(O(nlog_2n)\)
- 右端两个元素相等且均小于关键字,交换时相对位置互换
int func(vector<int>& a, int left, int right) {
int temp = a[left];
while (left < right) {
while (left < right && a[right] > temp) right--;
a[left] = a[right];
while (left < right && a[left] <= temp) left++;
a[right] = a[left];
}
a[left] = temp;
return left;
}
void quickSort(vector<int>& a, int left, int right) {
if (left < right) {
int pos = func(a, left, right);
quickSort(a, left, pos - 1);
quickSort(a, pos + 1, right);
}
}


选择排序
- 简单选择排序
void selectSort(vector<int>& a) {
int n = a.size();
for (int i = 0; i < n; i++) {
int k = i;
for (int j = i + 1; j < n; j++) {
if (a[j] < a[k]) k = j;
}
swap(a[i], a[k]);
}
}
- 堆排序
void downAdjust(int left, int right) {
int i = left, j = i * 2;
while (j <= right) {
if (j + 1 <= right && heap[j + 1] > heap[j]) j = j + 1;
if (heap[i] < heap[j]) {
swap(heap[i], heap[j]);
i = j;
j = i * 2;
} else break;
}
}
void upAdjust(int left, int right) {
int i = right, j = i / 2;
while (j >= left) {
if (heap[j] < heap[i]) {
swap(heap[i], heap[j]);
i = j;
j = i / 2;
} else break;
}
}
void createHeap() {
for (int i = n / 2; i >= 1; i--) downAdjust(i, n);
}
void heapSort() {
createHeap();
for (int i = n; i > 1; i--) {
swap(heap[i], heap[1]); // 此时heap[i]上为当前序列的最大值
downAdjust(1, i - 1);
}
}
归并排序和基数排序
- 归并排序(两路)
- 空间复杂度\(O(n)\)
对于\(N\)个元素进行\(k\)路归并排序时,排序的趟数\(m\)满足\(k^m=N\),从而\(m = log_kN\),考虑到\(m\)是整数,所以\(m = \lceil log_kN \rceil\)
void merge(vector<int>& a, int l1, int r1, int l2, int r2) {
vector<int> temp;
int i = l1, j = l2;
while (i <= r1 && j <= r2) {
if (a[i] <= a[j]) temp.push_back(a[i++]);
else temp.push_back(a[j++]);
}
while (i <= r1) temp.push_back(a[i++]);
while (j <= r2) temp.push_back(a[j++]);
for (int i = 0; i < temp.size(); i++) a[l1 + i] = temp[i];
}
void mergeSort(vector<int>&a, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(a, left, mid);
mergeSort(a, mid + 1, right);
merge(a, left, mid, mid + 1, right);
}
}

- 基数排序
- 关键字长度为n的线性表中结点的关键字由\(d\)元组\((k_j^{d - 1}, \cdots, k_j^0)\)组成,其中\(k_j^{d - 1}\)为最主位关键字,\(k_j^0\)为最次位关键字
- 最高位优先(MSD),按关键字权重递减依次逐层划分成更小的子序列,最后将子序列连成一个有序序列
- 最低位优先(LSD),按关键字权重递增依次进行排序,最后形成一个有序序列
- 基数排序从优先级最低的关键字位开始,依次排序,最后组合起来,在排高优先级的元素时,低优先级的低优先级的相对位置有序,注意,基数排序时按位排的,高位不足时补0,方便计算
- 空间复杂度\(O(r)\)(一共\(r\)个队列,\(r\)的取值由关键字如何分解决定)

各种内部排序算法的比较
- 若n较小,可以直接采用直接插入排序和简单选择排序
- 若文件的初始状态已经关键字基本有序,采用直接插入和冒泡排序
- 若n较大,则应采用时间复杂度为\(O(nlog_2n)\)的排序方法
- 稳定的:归并排序
- 不稳定的:快速排序和堆排序
- n很大,记录关键字位数较少且可以分解时,可以使用基数排序
- 当记录本身的信息量较大时,为避免耗费大量时间移动记录,可用链表作为存储结构


浙公网安备 33010602011771号