文章目录
排序算法
介绍
本人主要记录了上课时讲的排序算法,给出了一些原理和代码,仅供参考。
排序稳定性
对于值相同的两个元素,排序前后的先后次序不变,则称该方法为稳定性排序方法,否则,称为非稳定性排序方法。
插入排序
概念
第 i 趟排序将序列的第 i + 1个元素插入到一个长度为i、且已经按升值有序的子序列(ki-1,1, ki-1,2, …, ki-1,i)的合适位置,得到一个大小为 i + 1、且仍然按升值有序的子序列(ki,1, ki,2, …, ki,i+1)。
简单来说:就是一直保证前 i 个数是有序的
例子
可以看一个具体事例
代码实现
void insertSort(int k[], int n)
{
int i, j;
int temp;
for (i = 1; i < n; i++)
{
temp = k[i];
for (j = i - 1; j >= 0 && temp < k[j]; j--)
k[j + 1] = k[j];
// 插入到此时第 j 个元素的后面(此时的第 j 个元素的大小应该是小于 temp 的)
k[j + 1] = temp;
}
}
注意
1、排序的时间效率与什么直接有关?
主要与排序过程中元素之间的比较次数直接有关。
2、若原始序列为一个按值递增的序列,则排序过程中一共要经过多少次元素之间的比较?
由于每一趟排序只需要经过一次元素之间的比较就可以找到被插入元素的合适位置,因此,整个 n - 1 趟排序一共要经过 n - 1 次元素之间的比较。
3、若原始序列为一个按值递减的序列,则排序过程中一共要经过多少次元素之间的比较?
由于第 i 趟排序需要经过 i 次元素之间的比较才能找到被插入元素的合适位置,因此,整个 n - 1 趟排序一共要经过 ∑ i = 1 n − 1 i = n ( n − 1 ) 2 \sum_{i = 1}^{n-1}{i} = \frac{n(n-1)}{2} ∑i=1n−1i=2n(n−1) 次元素之间的比较。
所以:
稳定性: | 稳定 |
---|---|
时间代价: | 最佳情况 : n − 1 n-1 n−1 次比较, 0 交换, O ( n ) O(n) O(n) ; 最差情况 :比较次数和交换次数为 O ( n 2 ) O(n^2) O(n2) ; 平均情况 : O ( n 2 ) O(n^2) O(n2) |
空间代价: | O ( 1 ) O(1) O(1) |
稳定排序
代码优化
插入排序还可以用折半查找进行优化,以减少比较次数
void insertBSort(int k[], int n)
{
int i, j, low, high, mid;
int temp;
for (i = 1; i < n; i++)
{
temp = k[i];
low = 0;
high = i - 1;
while (low <= high)
{
mid = (low + high) / 2;
if (temp < k[mid])
high = mid - 1;
else
low = mid + 1;
}
// 以上是折半查找的思想
// 此时 high 指向比 temp 小的元素,low 指向要插入的位置
for (j = i - 1; j >= low; j--)
k[j + 1] = k[j];
k[low] = temp;
}
}
选择排序
概念
第 i 趟排序从序列的后 n - i + 1 个元素中一个值最小的元素,将其置于该 n - i + 1 个元素的最前面。
代码实现
void selectSort(int array[], int n)
{
int i, j, index;
int temp;
for (i = 0; i < n - 1; i++)
{
// index 记录的是后 n - i + 1 个中最小的元素
index = i;
for (j = i + 1; j < n; j++)
{
if (array[j] < array[index])
{
index = j;
}
}
// 如果最小的元素下标不等于要交换的元素下标
// 也就是 index 的值并未发生改变
// 交换
if (index != i)
{
temp = array[index];
array[index] = array[i];
array[i] = temp;
}
}
}
注意
选择排序法的元素之间的比较次数与原始序列中元素的分布状态无关。
这是由于:
无论原始序列为什么状态,第 i 趟排序都需要经过 n - i 次元素之间的比较,因此,整个排序过程中元素之间的比较次数为
∑
i
=
1
n
−
1
n
−
i
=
n
(
n
−
1
)
2
\sum_{i = 1}^{n-1}{n-i} = \frac{n(n-1)}{2}
i=1∑n−1n−i=2n(n−1)
稳定性: | 不稳定 |
---|---|
时间代价: | 交换次数 : n − 1 n-1 n−1 ; 比较次数 : n 2 n^2 n2 ; 总时间代价 : O ( n 2 ) O(n^2) O(n2) |
空间代价: | O ( 1 ) O(1) O(1) |
冒泡排序
概念
两两对比(只关心相同元素),每趟冒出一个最大元素
代码实现
void bubbleSort(int k[], int n)
{
int i, j;
int temp;
for (i = 0; i < n - 1; i++)
{
for (j = 0; j < n - i - 1; j++)
if (k[j] > k[j + 1])
{
temp = k[j];
k[j] = k[j + 1];
k[j + 1] = temp; /* 交换两个元素的位置 */
}
}
}
优化代码
主要是增加了一个 flag 标记,如果有一趟排序中,没有元素交换,表明已经排好序,退出循环
void bubbleSort(int k[], int n)
{
int i, j, flag;
int temp;
for (i = 0; i < n - 1; i++)
{
flag = 0;
for (j = 0; j < n - i - 1; j++)
if (k[j] > k[j + 1])
{
temp = k[j];
k[j] = k[j + 1];
k[j + 1] = temp; /* 交换两个元素的位置 */
flag = 1; // 如果交换了,flag = 1
}
if (flag == 0)
{
break;
}
}
}
注意
冒泡排序法的排序趟数与原始序列中数据元素的排列有关,因此,排序的趟数为一个范围,即[1…n-1]
冒泡排序方法比较适合于参加排序的序列的原始状态基本有序的情况
稳定性: | 稳定 |
---|---|
时间代价: | 最小时间代价为 O ( n ) O(n) O(n),最佳情况下只运行第一轮循环;一般情况下为 O ( n 2 ) O(n^2) O(n2) |
空间代价: | O ( 1 ) O(1) O(1) |
谢尔排序(Shell)
概念
1、首先确定一个元素的间隔数 gap
2、将参加排序的元素按照 gap 分隔成若干个子序列( 即分别把那些位置相隔为gap的元素看作一个子序列)
3、然后对各个子序列采用某一种排序方法进行排序
4、此后减小 gap 值,重复上述过程,直到 gap < 1
一般来说,gap 的减小方法采用以下方式
g
a
p
1
=
[
n
2
]
gap_1 = [\frac{n}{2}]
gap1=[2n]
g a p i = [ g a p i − 1 2 ] gap_i = [\frac{gap_i-1}{2}] gapi=[2gapi−1]
简单来说:就是一种“分而治之”的思想,对一定间隔数的新序列进行排序,而不是整体排序
代码实现
由于谢尔排序的内部排序算法还是要依靠简单的排序方法,所以给出冒泡和插入的谢尔排序实现
冒泡排序实现
void shellSort(int k[], int n)
{
// 注意此处的 i、j 并不完全等同于冒泡排序的 i、j
int i, j, flag, gap = n;
int temp;
while (gap > 1)
{
gap = gap / 2;
do
{
flag = 0; /* 每趟排序前,标志flag置0 */
for (i = 0; i < n - gap; i++)
{
// i、j 是间隔为 gap 的相邻数
j = i + gap;
if (k[i] > k[j])
{
temp = k[i];
k[i] = k[j];
k[j] = temp;
flag = 1;
}
}
} while (flag != 0);
}
}
插入排序实现
void shellSort(int k[], int n)
{
int i, j, gap = n;
int temp;
while (gap > 1)
{
gap = gap / 2;
// 使用插入排序实现子序列排序
for (i = gap; i < n; i++)
{
temp = k[i];
for (j = i; j >= gap && k[j - gap] > temp; j -= gap)
{
k[j] = k[j - gap];
}
k[j] = temp;
}
}
}
注意
稳定性: | 不稳定 |
---|---|
时间代价: | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)与 O ( n 2 ) O(n^2) O(n2)之间 |
空间代价: | O ( 1 ) O(1) O(1) |
堆排序(Heap)
堆的定义
首先满足是一个完全二叉树
n个元素的序列 k 1 , k 2 , k 3 , , k n k_1,k_2,k_3,\qquad,k_n k1,k2,k3,,kn
当且仅当满足下列公式为大顶堆
k
i
≥
k
2
i
k
i
≥
k
2
i
+
1
k_i \ge k_{2i} \qquad k_i\ge k_{2i+1}
ki≥k2iki≥k2i+1
当且仅当满足下列公式为小顶堆
k
i
≤
k
2
i
k
i
≤
k
2
i
+
1
k_i \le k_{2i} \qquad k_i\le k_{2i+1}
ki≤k2iki≤k2i+1
举例为:50 23 41 20 19 36 4 12 18(大顶堆)
排序概念
第 i 趟排序将序列的前 n - i + 1 个元素组成的子序列转化为一个堆积,然后将堆的第一个元素与堆的最后那个元素交换位置。
排序步骤
- 将原始序列转换为第一个堆。
- 将堆的第一个元素与堆积的最后那个元素交换位置。(即“去掉”最大值元素)
- 将“去掉”最大值元素后剩下的元素组成的子序列重新转换一个新的堆。
- 重复上述过程的第 2 至第 3 步 n - 1 次。
例子
首先建立了一个原始堆
然后将 50 与 10 调换,并从堆中“剔除” 50 ,此时只是“假”剔除
然后前 n - 1 个数恢复堆的顺序 ,并进行下一次的交换
如此循环,最后可得从小到大的顺序
代码实现
/*
下调整结点i的位置,使得其祖先结点值都比其大。
如果一棵树仅根结点i不满足堆条件,通过该函数可将其调整为一个堆。
*/
/*
i : 被调整的二叉树的根的序号
n : 被调整的二叉树的结点数目
*/
void adjust(int k[], int i, int n)
{
int j;
int temp;
temp = k[i]; // 记录根节点
j = 2 * i + 1; // j 此时代表左孩子
while (j < n)
{
// 如果右孩子比做孩子大,j 变为右孩子
if (j + 1 < n && k[j] < k[j + 1])
j++;
// 如果根节点小于大的那一个孩子,交换,使得根节点永远小于所有祖父
if (temp < k[j])
{
k[(j - 1) / 2] = k[j];
j = 2 * j + 1;
}
else
break;
}
k[(j - 1) / 2] = temp;
}
void heapSort(int k[], int n)
{
int i;
int temp;
// 从最后一个节点的父亲开始调整,使其成为一个堆
for (i = n / 2 - 1; i >= 0; i--)
adjust(k, i, n);
for (i = n - 1; i >= 1; i--)
{
temp = k[i];
k[i] = k[0];
k[0] = temp;
adjust(k, 0, i);
}
}
注意
稳定性: | 不稳定 |
---|---|
时间代价: | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) |
空间代价: | O ( 1 ) O(1) O(1) |
二路归并排序(Merge)
概念
将两个位置相邻、并且各自按值有序的子序列合并为一个按值有序的子序列的过程称为二路归并,也是一种分而治之的思想。
代码实现
需要注意的是,需要一个tmp的临时数组来保存每一次合并后的数组。
/*
将两个位置相邻且按值有序子序列合并为一个按值有序的序列
*/
void merge(int x[], int tmp[], int left, int leftEnd, int rightEnd)
{
int i = left, j = leftEnd + 1, q = left;
while (i <= leftEnd && j <= rightEnd)
{
if (x[i] <= x[j])
{
tmp[q++] = x[i++];
}
else
{
tmp[q++] = x[j++];
}
}
while (i <= leftEnd)
{
tmp[q++] = x[i++];
}
while (j <= rightEnd)
{
tmp[q++] = x[j++];
}
/*将合并后内容复制回原数组*/
for (i = left; i <= rightEnd; i++)
{
x[i] = tmp[i];
}
}
void mSort(int k[], int tmp[], int left, int right)
{
int center;
// 递归思想逐一合并
if (left < right)
{
center = (left + right) / 2;
mSort(k, tmp, left, center);
mSort(k, tmp, center + 1, right);
merge(k, tmp, left, center, right);
}
}
void mergeSort(int k[], int n)
{
int *tmp = (int *)malloc(sizeof(int) * n);
if (tmp != NULL)
{
mSort(k, tmp, 0, n - 1);
free(tmp);
}
else
{
printf("error!\n");
}
}
注意
稳定性: | 稳定 |
---|---|
时间代价: | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) (不依赖原始输入数据的情况,最大、最小以及平均时间代价都相同) |
空间代价: | O ( n ) O(n) O(n) |
快速排序法(Quick)
概念
从当前参加排序的元素中任选一个元素(通常称为分界元素pivot)与当前参加排序的元素进行比较,凡是小于分界元素的元素都移到分界元素的前面,凡是大于分界元素的都移到分界元素的后面,分界元素将当前参与排序的元素分为两个部分,而分界元素处于排序的最终位置,然后,分别对这两部分的大小大于1的部分重复上诉过程直到结束。
分界元素的选择
可以选第一个或者最后一个、或位置居中的那个元素作为分界元素
关键
每轮排序至少可以确定一个元素到最终位置
步骤
-
反复执行动作—— i = i + 1,直到 k[s] 小于等于 k[i] 或者 i = t
反复执行动作—— j = j - 1 ,直到 k[s] 大于等于 k[j] 或者 j = s
-
若 i < j ,则 k[i] 与 k[j] 交换位置,转到第一步
-
若 i 大于等于 j ,则 k[s] 与 k[j] 交换位置,至此,分界元素 k[s] 的最终位置已经被确定,然后对 k[s] 的左右两部分分别进行上述过程
代码实现
void swap(int *i, int *j)
{
int k = *i;
*i = *j;
*j = k;
}
void quick(int k[], int left, int right)
{
int i, j;
int pivot; // 分界元素
// 保证排序的长度大于1
if (left < right)
{
i = left;
j = right + 1;
pivot = k[left]; // 分界元素设为数列第一个
while (1)
{
// 步骤一的内容
// 注意自增的顺序问题
while (k[++i] < pivot && i != right)
{
}
while (k[--j] > pivot && j != left)
{
}
if (i < j)
{
swap(&k[i], &k[j]);
}
else
{
break;
}
}
swap(&k[left], &k[j]);
// 此时第 j 个元素已经确定
quick(k, left, j - 1); // 对前一部分排序
quick(k, j + 1, right); // 对后一部分排序
}
}
void quickSort(int k[], int n)
{
quick(k, 0, n - 1);
}
C语言内置函数
void qsort(void *base, size_t n, size_t size,int (*cmp)(const void *, const void *))
其中,cmp函数里,返回值为负数时——表示不需要交换;返回值为正数时——表示需要交换
注意
稳定性: | 不稳定 |
---|---|
最差情况: | 时间代价: O ( n 2 ) O(n^2) O(n2) 空间代价: O ( n ) O(n) O(n) |
最佳情况: | 时间代价: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) 空间代价: O ( l o g 2 n ) O(log_2n) O(log2n) |
平均情况: | 时间代价: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) 空间代价: O ( l o g 2 n ) O(log_2n) O(log2n) |
桶排序(Bucket)(计数排序)
概念
主要不是通过比较来实现,有点类似于哈希表的思想
假设 a1, a2 … an 由小于 M 的正整数组成,桶排序的基本原理是使用一个大小为 M 的数组 C (初始化为0,称为桶bucket),当处理 ai 时,使 C[ai] 增1。最后遍历数组 C 输出排序后的表。
缺点
需要知道数组里最大的元素
代码实现
// M 指的是数组中最大的元素
void bucketSort(int k[], int n)
{
int temp[M] = {0};
int i, j;
for (i = 0; i < n; i++)
{
temp[k[i]]++;
}
for (i = 0, j = 0; i < M; i++)
{
if (temp[i])
{
k[j++] = i;
}
}
}
优点
桶排序是最快速、最简单的排序,不受排序时间下限 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) 的影响,它的时间复杂度是 O ( M + n ) O(M+n) O(M+n)