1.冒泡排序
1.1 思路
从左往右两两比较,将最大的数放在待排序元素的最后一位,重复此操作,直到数组有序。
例如现有一个数组为[8,6,12,4,3],进行第一次冒泡排序的过程如下图所示。
排序后的结果为[6,8,4,3,12],这样就将最大的元素放到了数组最后,待排序的数量减一,然后对待排序的数组重复上述操作。
1.2 代码及优化代码
void bubbleSort(vector<int>& v) {
int n = v.size();
for (int i = 1; i < n; i++) { // 代表排序趟数
for (int j = 0; j < n - i; j++) { // 待排序序列下标
if (v[j] > v[j + 1])
swap(v[j], v[j + 1]);
}
}
}
优化思路:如果在某次循环中没有进行交换操作,则说明该数组已经是有序的。
优化代码:
void bubbleSort(vector<int>& v) {
int n = v.size();
for (int i = 1; i < n; i++) {
bool flag = 0; //用来标记是否进行交换操作
for (int j = 0; j < n - i; j++) {
if (v[j] > v[j + 1]) {
swap(v[j], v[j + 1]);
flag = 1;
}
}
if (flag == 0) //如果没有发生交换退出整个循环
break;
}
}
1.3 时间复杂度及稳定性分析
时间复杂度:
最好情况:O(n) 本身就是有序的,只需要进行一趟排序,n-1次比较。
最坏情况:O() 数组逆序,n-1趟排序。
稳定性:稳定
2.选择排序
2.1 思路
每次选择一个最大的元素,将其放到待排序数组的最后一个位置。用一个变量index(初始化为0)记录最大元素的下标,遍历到待排序数组的末尾时,和这个元素进行交换,重复此操作。
2.2 代码
void selectSort(vector<int>& v) {
int n = v.size();
for (int i = 1; i < n; i++) {
int index = 0; //用index记录最大下标
for (int j = 1; j <= n - i; j++) {
if (v[j] > v[index])
index = j;
}
if (index != n - i) //如果最后一个元素不是最大元素就交换
swap(v[index], v[n - i]);
}
}
2.3 时间复杂度及稳定性分析
时间复杂度:O() 没有最好和最坏
稳定性:不稳定
3.插入排序
3.1 思路
将整个数组划分为两区,有序区和无序区,每次从无序区中取出一个元素,将其插入到有序区的适当的位置,在最开始的时候有序区只有一个元素,无序区有n-1个元素。
每遍历一次,有序区的元素个数增加一个,无序区的元素个数减少一个,重复n-1次,完成排序。
以下面这个图为例子,一个大小为5的数组,一共要进行四趟循环。红色区域代表有序区,白色区域代表无序区,白色区域的第一个元素表示的是要插入的那个元素。
3.2 代码
void insertSort(vector<int>& v) {
int n = v.size();
for (int i = 1; i < n; i++) {
int j = i - 1; // 表示有序区的最后一个元素
int temp = v[i]; // 存无序区的第一个元素,防止后移时被覆盖
for ( : j >= 0; j--) {
if (v[j] > temp) {
v[j + 1] = v[j]; // 有序区元素向后挪
} else
break;
}
v[j + 1] = temp; // 插入那个元素
}
}
3.3 时间复杂度及稳定性分析
时间复杂度:
最好情况:O(n) ,数组本身有序,每趟只需要进行一次比较
最坏情况:O() ,数组逆序,有多少元素就需要进行多少次比较
稳定性:稳定
4.计数排序(桶排序)
4.1 思路
另外开辟一个数组,用来统计原数组中每个元素出现的次数。然后遍历原数组,将遍历到的元素覆盖到原数组即可完成排序。
计数排序的步骤为:
1.遍历原数组,找到最大值max
2.开辟数组大小为max+1大小的数组
3.遍历原数组,统计原数组中元素出现的次数(新数组的下标就代表值,数组中的元素大小代表元素出现的次数)
4.遍历新数组,覆盖原数组。(只要新数组的元素大小不为0,就往元素组中插入该元素的下标,同时让其大小减一,直到它等于0)
如下图的数组,先找到最大值8,然后开辟一个大小为9的数组,遍历原数组统计元素出现的次数,然后遍历新数组覆盖原数组。
4.2 代码
int maxval(vector<int>& v) { //找原数组的最大值
int max = v[0];
int n = v.size();
for (int i = 1; i < n; i++)
if (v[i] > max)
max = v[i];
return max;
}
void countSort(vector<int>& v) {
int n = v.size();
int max = maxval(v);
vector<int> bucket(max + 1); //开辟大小为max+1的新数组,用于统计元素出现的个数
for (int i = 0; i < n; i++) { //统计元素出现的个数
bucket[v[i]]++;
}
int index = 0;
for (int i = 0; i <= max; i++) { //遍历新数组,覆盖原数组
while (bucket[i]--) {
v[index++] = i;
}
}
}
4.3 时间复杂度及稳定性分析
时间复杂度:O(n)
稳定性:默认是稳定性的,没有发生交换。
5.堆排序
5.1 思路
1.将待排序数组形象成一个堆结构,并将其调整为最大堆(最小堆)
(堆结构:完全二叉树,做孩子的下标是2i+1,右孩子2i+2)
(最大堆:任何一个父亲结点的值都大于其孩子结点)
2.将堆顶元素和待排序数组最后一个元素进行交换
3.待排序的数据量减一,将排序数组重新调整为最大堆结构,重复上述步骤,循环
n-1次将所有数据排序完成。
以下述数组为例,图一先初始化堆结构
然后再对这个堆结构进行调整,将其转化成最大堆,过程如果所示
先从最后一个父亲结点开始调整,如图1,发现子节点比父亲结点大,就进行交换。然后对倒数第二个父亲结点调整,以此类推。最后得到最终的最大堆结构,将堆顶的元素与最后一个元素进行交换,待排序的数组减少1,下次再进行最大堆调整的堆结构就如图6所示。直到最后只剩一个元素,该数组完成排序。
5.2 代码
void adjust(vector<int>& v, int start, int end) { //调整最大堆
int father = start;
int child = 2 * father + 1; // 左孩子
while (child <= end) { // 保证孩子是存在的才能进行堆结构调整
if (child + 1 <= end && v[child + 1] > v[child])
child++;
// child是孩子当中最大值的小标
if (v[child] > v[father]) {
swap(v[chaild], v[father]);
father = child;
child = father * 2 + 1;
} else
break;
}
}
void heapSort(vector<int>& v) {
int n = v.size();
// 初始化形成最大堆
for (int i = n / 2 - 1; i >= 0; i--) { // 从最后一个父亲结点开始调整
adjust(v, i, n - 1);
}
// 拿堆顶元素和最后一个元素发生交换
for (int i = n - 1; i >= 1; i--) { // j表示的是待排序序列的最后一个元素的下标
swap(v[0], v[i]);
// 重新调整堆结构
adjust(v, 0, i - 1);
}
}
5.3 时间复杂度及稳定性分析
时间复杂度:O()
稳定性:不稳定
6.快速排序
6.1 思路
通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,已达到整个序列有序。一趟快速排序的具体过程可描述为:从待排序列中任意选取一个记录(通常选取第一个记录)作为基准值,然后将记录中关键字比它小的记录都安置在它的位置之前,将记录中关键字比它大的记录都安置在它的位置之后。这样,以该基准值为分界线,将待排序列分成的两个子序列。
步骤:1.从数组中选择一个元素,一般是数组元素的第一个值,这个元素被称为基准值
2.找出比基准值小的元素以及比基准值大的元素
3.重复上述步骤
6.2 代码
void quicksort(vector<int>&v,int start,int end){
if(start>=end){
return;
}
int temp=v[start]; //区域内的第一个元素定位基准值
int i=start-1,j=end+1;
int index=start;
while(index<j){
if(v[index]==temp){
index++;
}else if(v[index]>temp){
swap(v[--j],v[index]);
}else{
swap(v[++i],v[index++]);
}
}
quicksort(v,start,i);
quicksort(v,j,end);
}
6.3 时间复杂度及稳定性分析
时间复杂度:O()
稳定性:不稳定
7.归并排序
7.1 思路
思想:将两个有序的数组合并成一个有序的数组。
第一步:
将数组进行分解,当分解成单个元素为一组的时候才是组内有序的,
第二步:
将两两有序的数组进行合并,将两个有序数组合并成一个有序数组。重复第二步,直至排序完
成。
合并的步骤:先申请两数组合并后那么大小的空间,然后将两个排好序的数组逐一进行比较
往申请空间里面放。
7.2 代码
void merge(vector<int>& v, int L, int mid, int R) {//合并两个有序数组
int n = R - L + 1;
vector<int> temp(n);
int index = 0;
int i = L, j =mid + 1;//控制左右下标
while (i <= mid&&j<=R) {
if (v[i] <= v[j])
temp[index++] = v[i++];
else
temp[index++] = v[j++];
}
while (i <= mid) {
temp[index++] = v[i++];
}
while (j <= R) {
temp[index++] = v[j++];
}
i = L;
for (index= 0; index < n; index++) { //将temp数组复制给原数组
v[i++] = temp[index];
}
}
void merg(vector<int>& v,int L, int R){
if (L == R) return;//区域内只剩一个元素
//当区域内元素多于一个,需要找中间位置进行分割
int mid = (R - L) / 2 + L;
merg(v, L, mid);//向左分割
merg(v, mid + 1, R);//向右分割
merge(v, L, mid, R);//合并
}
7.3 时间复杂度及稳定性分析
时间复杂度:O()
稳定性:稳定
总结
名称 | 最差 | 最优 | 稳定性 |
冒泡排序 | O(n) | O(n2) | 稳定 |
选择排序 | O(n2) | 不稳定 | |
插入排序 | O(n) | O(n2) | 稳定 |
计数排序 | O(n) | 稳定 | |
堆排序 | O(nlogn) | 不稳定 | |
快速排序 | O(nlogn) | O(n2) | 不稳定 |
归并排序 | O(nlogn) | 稳定 |