排序的稳定性:
假设
k
i
=
k
j
k_i=k_j
ki=kj,且在排序序列中
r
i
r_i
ri领先于
r
j
r_j
rj,如果排序后
r
i
r_i
ri仍领先于
r
j
r_j
rj,则排序方法是稳定的,否则,排序是不稳定的。
1.冒泡排序法
版本一
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int n;
cin >> n;
vector<int> v(n);
for (int i = 0; i < n; i++)cin >> v[i];
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j < n ; j++)
{
if (v[i] > v[j])
swap(v[i], v[j]);
}
}
for (auto i : v)
cout << i << " ";
}
通过比较当前下标与下标以后的所有元素,确定当前的最小值,每一个i所在的循环均可以确定一个最小值。
版本二
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int n;
cin >> n;
vector<int> v(n);
for (int i = 0; i < n; i++)cin >> v[i];
for (int i = 0; i < n; i++)
{
for (int j = n - 2; j >= i; j--)
{
if (v[j] > v[j + 1])
swap(v[j], v[j + 1]);
}
}
for (auto i : v)
cout << i << " ";
}
从最后一项开始,两两进行比较(第j项与第j+1项进行比较),如果前一项大于后一项,前一项则为较小者,直到循环到i,确定最小值。
复杂度分析:
数组长度为n,第i次循环需要进行,n-i-1 次比较
∑
i
=
0
n
−
1
(
n
−
i
−
1
)
=
(
n
−
1
)
∗
n
2
=
O
(
n
2
)
\sum_{i=0}^{n-1}(n-i-1) =\frac{(n-1)*n}{2} =O(n^2)
∑i=0n−1(n−i−1)=2(n−1)∗n=O(n2)
2. 简单选择排序
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int n;
cin >> n;
vector<int> v(n);
for (int i = 0; i < n; i++)cin >> v[i];
for (int i = 0; i < n; i++)
{
int mini = i;
for (int j = i+1; j < n; j++)
{
if (v[mini] > v[j])
mini = j;
}
if (i != mini)
swap(v[mini], v[i]);
}
for (auto i : v)
cout << i << " ";
}
与冒牌排序相比,减少了交换的次数,效率略高于冒泡排序。第i次循环确定第i个最小值,与后续n-i-1个元素进行比较。
复杂度分析:
数组长度为n,第i次循环需要进行,n-i-1 次比较
∑
i
=
0
n
−
1
(
n
−
i
−
1
)
=
(
n
−
1
)
∗
n
2
=
O
(
n
2
)
\sum_{i=0}^{n-1}(n-i-1) =\frac{(n-1)*n}{2} =O(n^2)
∑i=0n−1(n−i−1)=2(n−1)∗n=O(n2)
3. 直接插入排序
将一个记录插入到已经排好序的有序表中,从而得到一个新的,记录数增1的有序表。
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int n;
cin >> n;
vector<int> v(n);
for (int i = 0; i < n; i++)cin >> v[i];
int j;
for (int i = 1; i < n; i++)
{
if (v[i] < v[i - 1])
{
int temp = v[i];
// 存较小的数
for (j = i - 1; v[j] >= temp; j--)
{
v[j + 1] = v[j];
if (j == 0)
{
j--;
break;
}
}
v[j+1] = temp;
}
}
for (auto i : v)
cout << i << " ";
}
这个排序算法,个人感觉有点绕。先判断对于第i个数与i-1个数的大小关系,如果 a[i]<a[i-1] ,说明a[i-1]位于a[i]之前,所以从 j=i-1开始,循环至找到一个数小于a[i]停止,a[j] = a[j-1],第j-1的位置上的数向前挪一位置,再将一开始保存的a[i]存入a[j+1]中
复杂度分析:
下标从1开始
若本身就是有序,则只需要比较n-1次。
若完全逆序,对于第i个数,前面有i-1个数,同时加上temp共i个数,需要比较i次,因为从i = 2开始比较
∑
i
=
2
n
=
(
n
+
2
)
(
n
−
1
)
2
\sum_{i=2}^{n}=\frac{(n+2)(n-1)}{2}
∑i=2n=2(n+2)(n−1)
以上是时间复杂度为 O ( n 2 ) O(n^2) O(n2)的排序算法。接下来将要介绍改进的排序算法。
4.希尔排序
希尔排序就是以一个增量(该增量逐渐递减至一)进行插入排序。
该实例以增量序列:
i
n
c
r
e
m
e
n
t
/
3
+
1
increment / 3+1
increment/3+1
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int n;
cin >> n;
vector<int> v(n+1);
for (int i = 1; i <= n; i++)cin >> v[i];
int increment = n;
// 初始增量为数组长度
do
{
increment = increment / 3 + 1;
// 增量计算4->2->1
//从increment至n 进行插入排序
// t从increment+1 开始保证 t-increment存在
for (int t = increment + 1; t <= n; t++)
{
if (v[t - increment] > v[t])
{
v[0] = v[t];
int j;
// 从t-increment 开始,以increment为增量进行插入排序
// 不断进行循环
for (j = t - increment; j >0 &&v[j] > v[0]; j -= increment)
{
v[j + increment] = v[j];
}
// 退出循环时是位置j是小于v[0]的
v[j + increment] = v[0];
}
}
} while (increment>1);
for (int i = 1; i <= n; i++)
cout << v[i] << " ";
}
希尔排序是不稳定的排序方法,因为如果两个相等的元素存在,因为交换是跳跃式交换,很有可能他们的相对位置发生改变。
时间复杂度分析:根据增量序列不同,时间复杂度也不同,最好情况 O ( N 1.3 ) O(N^{1.3}) O(N1.3),最坏情况是 O ( N 2 ) O(N^{2}) O(N2),平均情况 O ( N l o g ( N ) ) O(Nlog(N)) O(Nlog(N))~ O ( N 2 ) O(N^{2}) O(N2)
5.堆排序
堆:堆是一个完全二叉树,每个节点的的值均大于或等于其左右孩子的值被称为大顶堆。每个节点的值均小于等于其左右孩子的值被称为小顶堆。
根节点标号为1,设有n个节点,则数的高度为 ⌊ log 2 n ⌋ + 1 \lfloor \log_2n \rfloor + 1 ⌊log2n⌋+1 ,若节点的标号为k,且存在左右子节点,则左子标号为 2 k 2k 2k,右子标号为 2 k + 1 2k+1 2k+1。
#include<iostream>
#include<queue>
#include<vector>
using namespace std;
void heapadjust(vector<int>& v, int index,int length)
{
int temp = v[index];
// 暂存需要交换的节点
for (int i = index * 2; i <= length; i *= 2)
{
if (i<length&&v[i] < v[i + 1])
i++;
// i==length 只有左子
if (temp >= v[i])
break;
v[index] = v[i];
index = i;
// 交换后的节点位置
}
v[index] = temp;
}
int main()
{
int n;
cin >> n;
vector<int>v(n+1);
for (int i = 1; i <= n; i++)
cin >> v[i];
int length = v.size() - 1;
for (int i = n / 2; i >= 1; i--)
heapadjust(v, i,length);
for (int i = length; i > 1; i--)
{
swap(v[1], v[i]);
heapadjust(v, 1, i - 1);
}
for (int i = 1;i<=n;i++)
printf("%d ", v[i]);
}
因为堆排序是跳跃式的比较,所有堆排序也是不稳定的。
构建堆需要
O
(
n
)
O(n)
O(n)的时间复杂度,因为共有
n
2
\frac{n}{2}
2n个非叶子节点,每个非叶子节点只需要与其左右子比较。
在在正式排序时,第i次取堆顶记录需要 log ( n − i ) = log ( n ) \log(n-i)=\log(n) log(n−i)=log(n)的时间,共需要n次,所以总时间复杂度为 O ( n log ( n ) ) O(n\log(n)) O(nlog(n))
上面堆排序是通过非递归算法进行的,且下标从一开始。那么如果我们用递归的方法,且下标从零开始怎么办呢?
#include<iostream>
#include<vector>
using namespace std;
void heapadjust(vector<int>& v,int posi,int last)
{
// posi 为当前调整的位置
// last 为数组中最后一个元素的位置
if (posi < 0)
return;
int larger_posi = posi,left = posi * 2 + 1, right = posi * 2+2;
if (left <= last && v[larger_posi] < v[left])
larger_posi = left;
if (right <= last && v[larger_posi] < v[right])
larger_posi = right;
swap(v[posi], v[larger_posi]);
heapadjust(v, posi - 1 ,last);
}
int main()
{
int n;
cin >> n;
vector<int>v(n);
for (int i = 0; i < n; i++) cin >> v[i];
heapadjust(v, (n-2) / 2, n-1);
for (auto i : v)
cout << i << " ";
cout << endl;
for (int i = n - 1; i >= 0; i--)
{
swap(v[0], v[i]);
heapadjust(v, (i- 1) / 2, i - 1);
}
for (auto i : v)
cout << i << " ";
}
如果下标从0开始,数组长度为n,那么最后一个非叶子节点的下标为
⌊
n
−
2
2
⌋
\lfloor\frac{n-2}{2}\rfloor
⌊2n−2⌋。
且对于一个非叶子节点如果下标从零开始,该节点的下标为x,则其左子为
2
∗
x
+
1
2*x+1
2∗x+1,其右子为
2
∗
x
+
2
2*x+2
2∗x+2。
6.归并排序
归并排序就是不断的将数组进行二分,直到分成的子数组只剩下一个元素,之后合并两个元素使之成为有序的数组的过程。
#include<iostream>
#include<vector>
using namespace std;
const int N = 1e6 + 10;
int q[N];
int temp[N];
void mergesort(int l, int r, int q[])
{
if (l >= r)
return;
int mid = l + r >> 1;
mergesort(l, mid, q);
mergesort(mid + 1, r, q);
int k=0,i = l, j = mid + 1;
while (i <= mid && j <= r)
if (q[i] <= q[j]) temp[k++] = q[i++];
else temp[k++] = q[j++];
while (i <= mid) temp[k++] = q[i++];
while (j <= r) temp[k++] = q[j++];
for (int i = l, j = 0; i <= r; i++)
q[i] = temp[j++];
}
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i++) cin >> q[i];
mergesort(0, n - 1, q);
for (int i = 0; i < n; i++) printf("%d ", q[i]);
}
时间复杂度分析,数组长度为n,需要进行 ⌊ log n ⌋ + 1 \lfloor\log{n}\rfloor + 1 ⌊logn⌋+1次拆分数组,每次拆分完后合并数组需要花费 O ( n ) O(n) O(n)的时间复杂度,所以时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)
7.快速排序
#include<iostream>
#include<vector>
#include<string>
using namespace std;
int a[10], b[10];
void quick_sort(vector<int>& v, int l, int r)
{
if (l >= r)
return;
int i = l - 1, j = r + 1;
int mid = v[l];
while (i < j)
{
do i++; while (v[i] < mid);
do j--; while (v[j] > mid);
if (i < j) swap(v[i], v[j]);
}
quick_sort(v, l, i);
quick_sort(v, i + 1, r);
}
int main()
{
int n;
cin >> n;
vector<int> v(n);
for (int i = 0; i < n; i++) cin >> v[i];
quick_sort(v, 0, n - 1);
for (auto i : v)
printf("%d ", i);
}
模板。如果选取枢纽元为左边界,则递归时为(l,i),(i+1,r)