给你N个自然数,编程输出排序后的这N个数。
第一行是整数的个数N(N<=100)。第二行是用空格隔开的N个数。
排序输出N个数,每个数间用一个空格间隔。
5
9 6 8 7 5
5 6 7 8 9
将待排数组分为“已排序”和“未排序”两个部分,R[0,1...i-1]
前面序列是已经排好的有序区,R[i,...n]
后面的序列是未排序的无序区,直接插入排序每次操作将当前无序区的首元素R[i]
插入到有序区R[0,1...i-1]
的适当位置,使得R[0,1...i]
成为新的有序区,减小无序区,直至无序区为空,从而全部数据有序!
#include
#include
using namespace std;
bool cmp(int a, int b) { return a < b; }
int main() {
int N;
cin >> N;
vector<int> R;
for (int i = 0; i < N; i++) {
int a;
cin >> a;
R.push_back(a);
}
for (int i = 1; i < N; i++) {
int temp = R[i];//用temp临时存储待排元素
int j = i - 1;//让temp从i-1开始逐个向前比较
while (j >= 0 && cmp(temp, R[j])) {
R[j + 1] = R[j];
j--;
}
R[j + 1] = temp;
}
for (int i = 0; i < N; i++) {
cout << R[i] << " ";
}
return 0;
}
这里将比较逻辑模块化:
bool cmp(int a, int b) { return a > b; }
若未来需要修改排序规则(降序排序)只需要将cmp
函数中的>
修改为<
即可!
直接插入排序由两重循环构成,对于具有n
个元素的数组R
,外循环要进行n-1
趟排序(1到n-1
),在每趟排序中,仅当待插入序列元素R[i]
小于有序区尾元素时才进入内层循环,因此直接插入排序的时间性能与初始排序表相关。
O(n)
i
次比较,等差数列n(n-1)/2
,时间复杂度为O(n^2)
R[i]
插入到有序区的中间位置R[0,1...i-1]
,等差数列n(n-1)/4
,时间复杂度为O(n^2)
。 由于其平均时间性能接近最坏性能,所以是一种低效的排序方法。在该算法中只使用了i
,j
,temp
三个辅助变量,与问题规模n
无关,故空间复杂度为O(1)
,是一个就地排序算法,同时相等时排序不变,是种稳定的排序算法。
在直接插入排序的基础上,用折半查找的方法找到无序区元素插入的位置。
#include
#include
using namespace std;
int main() {
int N;
cin >> N;
vector<int> R;
for (int i = 0; i < N; i++) {
int a;
cin >> a;
R.push_back(a);
}
for (int i = 1; i < N; i++) {
int temp = R[i];
int low = 0, high = i - 1;
while (low <= high) {//退出循环时low=high+1
int mid = (low + high) / 2;
if (temp > R[mid]) {
low = mid + 1;
}
else {
high = mid - 1;
}
}
for (int j = i - 1; j >= high + 1; j--) {
R[j + 1] = R[j];
}
R[high + 1] = temp;
}
for (int i = 0; i < N; i++) {
cout << R[i] << " ";
}
return 0;
}
平均情况下时间复杂度为O(n^2)
,从时间复杂度来看,折半插入与直接插入排序相同,但是当元素数量较多时,折半查找优于顺序查找,减少了关键字比较的次数,所以折半插入排序优于直接插入排序。同时其空间复杂度为O(1)
,也是种稳定的排序算法。
希尔排序是一种采用分组插入排序的方法,先取一个小于n
的整数 d 1 {d}_{1} d1作为第一个增量,将全部元素R
中所有相距为 d 1 {d}_{1} d1的元素分成一组,在组内进行直接插入排序,然后取第二个增量 d 2 {d}_{2} d2( d 2 {d}_{2} d2< d 1 {d}_{1} d1),重复上述的分组和排序,直至增量 d t {d}_{t} dt=1,即所有的元素为一组,在进行一次直接插入排序,从而使得所有元素有序!
从理论上讲,增量序列的取值只要满足初始值小于n
再递减并且最后等于1
就可以了。最常见的是Shell增量序列,即取 d 1 {d}_{1} d1=n/2
, d i + 1 {d}_{i+1} di+1= d i {d}_{i} di/2,直到 d t {d}_{t} dt=0为止!
#include
#include
using namespace std;
int main() {
vector <int> R;
int N;
cin >> N;
for (int i = 0;i < N;i++) {
int a;
cin >> a;
R.push_back(a);
}
int d = N / 2;
while (d != 0) {
for (int i= d;i< N;i++) {
int temp = R[i];
int j = i;
while (j >= d && R[j - d] > temp) {
R[j] = R[j - d];
j -= d;
}
R[j] = temp;
}
d /= 2;
}
for (int i = 0;i < N;i++) {
cout << R[i] << " ";
}
return 0;
}
由于希尔排序的增量序列不确定,算法的时间复杂度难以分析,我们一般认为其平均时间复杂度为O(n^1.58)
,希尔排序通常要比直接插入排序快,在希尔排序中我们使用了i
,j
,temp
,d
四个辅助变量,与问题规模n
无关,故算法空间复杂度为O(1)
,也就是说是一种就地排序。但是希尔排序过程中相同元素的相对位置可能发生变化,因而是一种不稳定的排序算法。
在排序表中取一个元素为基准(一般是第一个),将基准归位(即将基准放在他最终的位置上),同时将所有小于基准的元素放到基准的前面(构成左子表),将所有大于基准的元素放到基准的后面(构成右子表),这个过程叫作划分。然后用递归的思想对左、右子表分别重复上述过程,直至每个子表只有一个元素或空为止。
快速排序每次仅将一个元素归位,在最后一趟排序结束前并不产生明确的连续有序区。
#include
#include
using namespace std;
int partition(vector <int>&arr, int low, int high) {
int base = arr[low];
int i = low + 1, j = high;
while (i <= j) {
while ( i <= j && arr[i] <= base) {
i++;
}
while (i <= j && arr[j] >= base) {
j--;
}
if (i < j) {
swap(arr[i], arr[j]);
i++;
j--;
}
}
swap(arr[low], arr[j]);
return i;
}
void quicksort(vector <int>& arr, int low, int high) {
if (low >= high)return;
int pi = partition(arr, low, high);
quicksort(arr, low, pi-1);
quicksort(arr, pi+1, high);
}
int main() {
vector <int> R;
int N;
cin >> N;
for (int i = 0;i < N;i++) {
int a;
cin >> a;
R.push_back(a);
}
quicksort(R, 0, N - 1);
for (int i = 0;i < N;i++) {
cout << R[i] << " ";
}
return 0;
}
O(nlog2n)
。n-1
,则递归树的高度最高,性能最差,此时排序的时间复杂度为O(n^2)
。O(nlog2n)
,这接近最好情况,所以快速排序是一种高效的排序方法。 快速排序使用的是递归算法,尽管每一次划分仅仅使用固定的几个辅助变量,但是递归树的高度最好为O(log2n)
,对应最好的空间复杂度为O(log2n)
,最坏情况下递归树的高度为O(n)
,对应最坏的空间复杂度为O(n)
。
另外,快速排序是一种不稳定的排序算法。(STL的sort()
函数就是使用快速排序实现的,当划分的区间长度较小时,采用直接插入排序,所以sort()
是不稳定的,且时间复杂度为O(nlog2n)
)
堆排序是对选择排序的一种改进,采用二叉树来代替简单的选择方法来找最大或者最小元素,属于一种树形选择排序方法。我们采用数组隐式构建二叉树:
#include
#include
using namespace std;
void heapify(vector<int>& arr, int n, int root) {
int largest = root;
int left = 2 * root + 1;
int right = 2 * root + 2;
if (left < n && arr[left] > arr[largest])
largest = left;
if (right < n && arr[right] > arr[largest])
largest = right;
if (largest != root) {
swap(arr[root], arr[largest]);
heapify(arr, n, largest);
}
}
void heapSort(vector<int>& arr) {
int n = arr.size();
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
for (int i = n - 1; i > 0; i--) {
swap(arr[0], arr[i]);
heapify(arr, i, 0);
}
}
int main() {
int N;
cin >> N;
vector<int> arr(N);
for (int i = 0; i < N; i++) {
cin >> arr[i];
}
heapSort(arr);
for (int i = 0; i < N; i++) {
if (i > 0) cout << " ";
cout << arr[i];
}
cout << endl;
return 0;
}
堆排序的时间主要由建立初始堆和反复重建堆这两部分的时间构成,建立初始堆的时间复杂度为O(nlog2n)
,后面反复归位元素和重建堆的时间复杂度为O(nlog2n)
,因此最好、最坏、平均时间复杂度均为O(nlog2n)
。
堆排序只使用了固定的几个辅助变量,其算法的空间复杂度为O(1)
,同时是一种不稳定的排序算法。
通过多次将两个或两个以上的相邻有序表合并成一个新的有序表。可以分为二路归并、三路归并、多路归并排序。其中二路归并排序又可以分为自底向上和自顶向下两种方法。
二路归并先将R[0...n-1]
看成n
个长度为1
的有序子表,然后在进行两两相邻有序子表的合并,得到n/2
个长度为2
的有序子表,在进行两两有序子表的合并,以此类推,直到得到一个长度为n
的有序表为止。
二路归并时,先将两段有序合并到一个新的局部变量R1
中,待合并完成后再将R1
复制回R
中。
#include
#include
using namespace std;
void merge(vector<int>& arr, int left, int mid, int right) {
vector<int> temp(right - left + 1);
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
}
else {
temp[k++] = arr[j++];
}
}
while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= right) {
temp[k++] = arr[j++];
}
for (int p = 0; p < k; p++) {
arr[left + p] = temp[p];
}
}
void mergeSort(vector<int>& arr, int left, int right) {
if (left >= right) {
return;
}
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
int main() {
int N;
cin >> N;
vector<int> arr(N);
for (int i = 0; i < N; i++) {
cin >> arr[i];
}
mergeSort(arr, 0, arr.size() - 1);
for (int i = 0; i < N; i++) {
if (i > 0) cout << " ";
cout << arr[i];
}
cout << endl;
return 0;
}
在二路归并排序中,长度为n
的排序表需要做log2n
趟排序,对应的归并树高度为log2n+1
,每趟归并时间为O(n)
,故其时间复杂度的最好、最坏、平均情况都是O(nlog2n)
。
在归并排序中每次都需要用到局部变量R1
,最后一趟的排序一定是全部n
个元素参与归并,所以总的辅助空间复杂度为O(n)
。
同时Merge
算法不会改变相同关键字元素的相对次序,所以二路归并算法是一种稳定的排序方法!
有关冒泡排序、选择排序和sort()
函数排序的相关代码在:数据结构实验2中,有兴趣的可以直接传送门!
排序方法 | 平均情况 | 最坏情况 | 最好情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
直接插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
折半插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
希尔排序 | O(n^1.58) | \ | \ | O(1) | 不稳定 |
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
快速排序 | O(nlog2n) | O(n^2) | O(nlog2n) | O(log2n) | 不稳定 |
简单选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 |
归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 稳定 |
封面来源:Explaining EVERY Sorting Algorithm (part 1)