一. 冒泡排序
1. 思想:利用比较相邻的两个元素,发现两个数前者大于后者则进行交换,这样每一轮可以把最大数放到后面,只要做n轮便可以使得序列有序。
2. 举例,例如序列 8 7 3 4 5 0 1
第一轮:8 7 3 4 5 0 1
8 7
3 4 5 0 1 -> 7 8 3 4 5 0 1
7
8 3
4 5 0 1 -> 7 3 8 4 5 0 1
7 3
8 4
5 0 1 -> 7 3 4 8 5 0 1
7 3 4
8 5
0 1 -> 7 3 4 5 8 0 1
7 3 4 5
8 0
1 -> 7 3 4 5 0 8 1
7 3 4 5 0
8 1
-> 7 3 4 5 0 1
8 // 得到最大值8在最后一个位置
第二轮:7 3 4 5 0 1
………..
3. 从上面的例子我们可以知道,冒泡排序的时间复杂度为O(n^2),不需要额外的空间辅助,是一个稳定的算法
4. 示例代码
#include<iostream>
#include<algorithm>
using namespace std;
//冒泡排序
void BubbleSort(int *arrNum, int n){
//数据不合法
if(arrNum == NULL || n <= 0){
return;
}
for(int i = 0; i < n; i++){
for(int j = 0; j < (n-i-1); j++){
if(arrNum[j] > arrNum[j+1]){
swap(arrNum[j], arrNum[j+1]);
}
}
}
}
int main(){
//test
int arrNum[] = {8,7,3,4,5,0,1};
BubbleSort(arrNum, 7);
for(int i = 0; i < 7; i++){
cout<<arrNum[i]<<" ";
}
cout<<endl;
//输出 0 1 3 4 5 7 8
getchar();
return 0;
}
二. 插入排序
1. 思想:每次选择未排序序列的第一个数插入已经排好序的序列中,使得这个序列依然有序
2. 举例:
例如序列 8 7 3 4 5 0 1
第一次:未排序序列为8 7 3 4 5 0 1,第一个数是8,没有排好的序列,因此排好序列为8
第二次:未排序序列为7 3 4 5 0 1,第一个数7,排好序序列为8,则插入到8前面,序列为7 8
第三次:未排序序列为3 4 5 0 1,第一个数为3,排好序序列为3,则插入到7前面,序列为3 7 8
……..
3. 只要把所有的未排序的数全部插入到已经排好序的序列中,这样整个序列就是有序的。时间复杂度为O(n^2),是一个稳定的排序算法
4. 示例代码
#include<iostream>
#include<algorithm>
using namespace std;
//插入排序
void InsertSort(int *arrNum, int n){
//数据不合法
if(arrNum == NULL || n <= 0){
return;
}
for(int i = 1; i < n; i++){
int tmp = arrNum[i];
int j;
for(j = i-1; j >= 0; j--){
//如果比tmp大把值往后移动一位
if(arrNum[j] > tmp){
arrNum[j+1] = arrNum[j];
}
else{
break;
}
}
arrNum[j+1] = tmp;
}
}
int main(){
//test
int arrNum[] = {8,7,3,4,5,0,1};
InsertSort(arrNum, 7);
for(int i = 0; i < 7; i++){
cout<<arrNum[i]<<" ";
}
cout<<endl;
//输出 0 1 3 4 5 7 8
getchar();
return 0;
}
三. 选择排序
1. 思想:每次从未排序的序列中选择一个最小的数未排序序列第一个数交换,相当于每次选择一个最小的数放到排好序的序列最后一个位子,当未排序序列为空的时候整个序列即为有序
2. 举例:例如序列8 7 3 4 5 0 1
第一次:序列 8 7 3 4 5 0 1,未排序序列最小的数0,和8交换,此时排好序序列为0
第二次:序列
0
7 3 4 5 8 1,未排序序列最小的数1,和7交换,此时排好序序列为0 1
第三次:序列
0 1
3 4 5 8 7,未排序序列最小的数3,本身不用交换,此时排好序序列为0 1 3
……………
3. 当未排序序列为空的时候说明整个序列为有序,时间复杂度为O(n^2),是一个不稳定的排序算法
4. 示例代码
#include<iostream>
#include<algorithm>
using namespace std;
//插入排序
void SelectSort(int *arrNum, int n){
//数据不合法
if(arrNum == NULL || n <= 0){
return;
}
for(int i = 0; i < n; i++){
int minNumIndex = i;
for(int j = i+1; j < n; j++){
if(arrNum[j] < arrNum[minNumIndex]){
minNumIndex = j;
}
}
//找到最小值直接和未排序第一个数交换
if(i != minNumIndex){
swap(arrNum[i], arrNum[minNumIndex]);
}
}
}
int main(){
//test
int arrNum[] = {8,7,3,4,5,0,1};
SelectSort(arrNum, 7);
for(int i = 0; i < 7; i++){
cout<<arrNum[i]<<" ";
}
cout<<endl;
//输出 0 1 3 4 5 7 8
getchar();
return 0;
}
四. 快速排序
1. 思想:利用一个基准arrNum[k]每次把区间[l,r]划分成两个部分[l,k-1],[k+1,r]使得[l,k-1]这个子区间的所有值全部小于等于arrNum[k],[k+1,r]这个区间的值全部大于等于arrNum[k]的值,然后递归对这两个子区间进行排序。
2. 举例:例如序列8 7 3 4 5 0 1,每次选择区间中间那个数作为基准
第一次:区间为[0, 6],选择基准为arrNum[3] = 4
(1)先把基准4和最后一个数交换,得到8 7 3 1 5 0
4
(2)设置两个指针p1和p2,p1用来枚举所有数,p2用来保存左子区间全部小于基准的下标,初始化p1和p2都是指向区间左端点
(3)手动推算结果如下
3. 快速排序时间复杂度为O(NlogN),是一个不稳定的算法,有可能会退化为O(n^2),不需要额外的空间
4. 示例代码
#include<iostream>
#include<algorithm>
using namespace std;
//划分区间
int Partition(int *arrNum , int l, int r){
//左子区间结束下标
int leftSeqIndex = l;
int mid = (l+r)>>1;
//把基准值交换到最后一个位置
swap(arrNum[mid], arrNum[r]);
//O(n)时间划分
for(int i = l; i < r; i++){
if(arrNum[i] < arrNum[r]){
if(leftSeqIndex < i){
swap(arrNum[leftSeqIndex], arrNum[i]);
}
++leftSeqIndex;
}
}
//把基准交换回原来的位置
swap(arrNum[leftSeqIndex], arrNum[r]);
return leftSeqIndex;
}
//快速排序
void QuickSort(int *arrNum, int l, int r){
//数据不合法
if(arrNum == NULL || l >= r){
return;
}
int index = Partition(arrNum, l, r);
if(l < index){
QuickSort(arrNum, l, index-1);
}
if(index < r){
QuickSort(arrNum, index+1, r);
}
}
int main(){
//test
int arrNum[] = {8,7,3,4,5,0,1};
QuickSort(arrNum, 0, 6);
for(int i = 0; i < 7; i++){
cout<<arrNum[i]<<" ";
}
cout<<endl;
//输出 0 1 3 4 5 7 8
getchar();
return 0;
}
五. 归并排序
1. 思想:利用分治的思想,每次把要排序的区间平均划分成两部分,然后递归排序,再合并这两个区间
2. 举例:例如序列8 7 3 1 5 0 4
3. 归并排序的时间复杂度为O(NlogN),是一个稳定的排序算法,但是需用一个和原来一样大的内存空间
4. 示例代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 10;
//合并
void Merge(int *arrNum , int l, int mid, int r){
int i = l;
int j = mid+1;
int pos = l;
int tmpArrNum[N];
while(i <= mid && j <= r){
if(arrNum[i] <= arrNum[j]){
tmpArrNum[pos++] = arrNum[i++];
}
else{
tmpArrNum[pos++] = arrNum[j++];
}
}
//把没有排完的数
while(i <= mid){
tmpArrNum[pos++] = arrNum[i++];
}
while(j <= r){
tmpArrNum[pos++] = arrNum[j++];
}
//把数据拷贝回到原来数组
for(int i = l; i <= r; i++){
arrNum[i] = tmpArrNum[i];
}
}
//归并排序
void MergeSort(int *arrNum, int l, int r){
//数据不合法
if(arrNum == NULL || l >= r){
return;
}
int mid = (l+r)>>1;
MergeSort(arrNum, l, mid);
MergeSort(arrNum, mid+1, r);
Merge(arrNum, l, mid, r);
}
int main(){
//test
int arrNum[] = {8,7,3,4,5,0,1};
MergeSort(arrNum, 0, 6);
for(int i = 0; i < 7; i++){
cout<<arrNum[i]<<" ";
}
cout<<endl;
//输出 0 1 3 4 5 7 8
getchar();
return 0;
}
六. 堆排序
1. 堆排序利用堆的性质,最小堆满足根结点的值比左右子树都小,最大堆满足根结点的值比左右子树都大
2. 一个无序系列建立最小堆方法是,从第一个叶子到根结点,每个结点往下调整,调整完所有结点之后即为最小堆
为什么叶子结点不用调整?因为叶子结点已经满足最小堆的性质
3. 一个无序序列调整为最小堆的过程,序列为8 7 3 4 5 0 1
4. 建立最小堆的时间复杂度为O(n),很多人认为是O(NlogN),为什么不是呢?证明如下
假设堆中有N个元素,则树高为H = logN,对于树高为h的结点建堆时间复杂度为O(H-h)。因此从最底层到根结点所有结点的建堆时间复杂度和为
S = 0*2^(H-1)+1*2^(H-2)+2*2^(H-3)+……..+(H-1)*2^0
<= 2^(H)+2^(H-1)+2^(H-2)+…….+2^0
<= 2^(H+1)
因为H = logN,则2^(H+1) = 2^H*2 = 2N,则时间复杂度为O(N)。
5. 建立最小堆代码
//向下调整
void AdjustDown(int *arrNum, int pos, int n){
int minIndex = pos;
int lsonIndex = pos<<1;
int rsonIndex = (pos<<1)+1;
if(lsonIndex <= n && arrNum[lsonIndex] < arrNum[minIndex]){
minIndex = lsonIndex;
}
if(rsonIndex <= n && arrNum[rsonIndex] < arrNum[minIndex]){
minIndex = rsonIndex;
}
if(minIndex != pos){
swap(arrNum[pos], arrNum[minIndex]);
AdjustDown(arrNum, minIndex, n);
}
}
//建立最小堆
void BuildHeap(int *arrNum, int n){
if(arrNum == NULL || n <= 0){
return;
}
//下标从1开始第一个非叶子结点编号为n/2
for(int i = n/2; i >= 1; i--){
//向下调整
AdjustDown(arrNum, i, n);
}
}
6. 堆排序只需要每次把最小堆的根结点值取出并删除根结点,然后把最后一个叶子结点补到根结点并删除指,然后从根结点往下调整即可,时间复杂为O(NlogN),是一个不稳定的排序算法,不需要额外空间
#include<iostream>
#include<algorithm>
using namespace std;
//向下调整
void AdjustDown(int *arrNum, int pos, int n){
int minIndex = pos;
int lsonIndex = pos<<1;
int rsonIndex = (pos<<1)+1;
if(lsonIndex <= n && arrNum[lsonIndex] < arrNum[minIndex]){
minIndex = lsonIndex;
}
if(rsonIndex <= n && arrNum[rsonIndex] < arrNum[minIndex]){
minIndex = rsonIndex;
}
if(minIndex != pos){
swap(arrNum[pos], arrNum[minIndex]);
AdjustDown(arrNum, minIndex, n);
}
}
//建立最小堆
void BuildHeap(int *arrNum, int n){
if(arrNum == NULL || n <= 0){
return;
}
//下标从1开始第一个非叶子结点编号为n/2
for(int i = n/2; i >= 1; i--){
//向下调整
AdjustDown(arrNum, i, n);
}
}
void HeapSort(int *arrNum, int n){
//不合法数据
if(arrNum == NULL || n <= 0){
return;
}
int pos = 1;
while(pos <= n){
//每次输出根节点的值即为最小
cout<<arrNum[1]<<" ";
//把最后一个结点交换上去
swap(arrNum[1], arrNum[n-pos+1]);
//从根结点向下调整
AdjustDown(arrNum, 1, n-pos);
++pos;
}
}
int main(){
//test
int arrNum[] = {-1, 8,7,3,4,5,0,1};
BuildHeap(arrNum, 7);
HeapSort(arrNum, 7);
//输出 0 1 3 4 5 7 8
getchar();
return 0;
}
总结:
稳定排序算法:冒泡排序、插入排序、归并排序、基数排序
不稳定排序算法:选择排序、快速排序、堆排序、希尔排序