文章内容繁多,且混有代码,记得配合目录观看,达到最佳学习体验!
前言
1.分析标准:性质+时间复杂度
性质分类:
就地排序/非就地排序
就地:只用到存储数据的空间,没有额外开辟空间
内部排序/外部排序
内部:待排序的数据能一次性放到内存中(就是内存16g够不够用)
(不能的话需要和外存交互,外部的这里只有归并)
稳定排序/不稳定排序
稳定:大小相同的数据,排序前后相对位置不变
2.统一的标准
数组存储 统一由下标1开始存储
数组排序 统一为排成从小到大
语言实现是C++
个别排序因为很少见,没有代码,只提供了思路,当然,绝大多数是有代码哒。
//基于交换的排序
//1.冒泡排序:
不断比较相邻两个元素的,如果这两个元素是乱序的,
//则交换位置,从而实现把(这一趟)(乱序区中)最大的数据放在最后面
//重复上面的过程(等于是从后向前变得有序,有序区在后方,随趟数增加而增加)
性质:就地的 稳定的 内部的
时间复杂度:O(n^2)
#include<iostream>
using namespace std;
int main()
{
int n; cin >> n;
int data[105];
for (int i = 1; i <= n; i++)
{
cin >> data[i];
}
for (int i = 1; i <= n - 1; i++)//遍历趟数
{
int flag = 0;//优化
for (int j = 1; j <= n - i; j++)
{
if (data[j] > data[j + 1])
{
swap(data[j], data[j + 1]);
flag = 1;//如果一趟没有发生交换,说明已经排好了
}
}
if (flag == 0)
{//排好了就直接输出
break;
}
for (int i = 1; i <= n; i++)
{
cout << data[i] << " ";
}
cout << endl;
}
return 0;
}
/*
8
3 1 8 4 2 6 1 7
*/
//2.快速排序:
先选一个基准数x,将比它小的数据放在它的前面,将比它大的数据放在它的后面,
//排好一趟后,x把序列分为两部分,这两部分内部可能还是乱序的,所以分别对这两部分进行快速排序......
// (递归的思路)
//基准数选择:(1)排序区间第一个(2)排序区间最后一个(3)中间位置(这里代码选第一个啦)
//代码思路:皮课51:00
//性质:就地的 内部的 不稳定的
时间复杂度:每趟划分等于是遍历一遍整个数列,一共排logn趟,所以时间复杂度为O(nlogn)
#include<iostream>
using namespace std;
void quick_sort(int* data, int l, int r)
{
if (l < r)
{
int i, j, x;//两个指向,一个基准数
i = l; j = r;
x = data[l];
while (i < j)//记住时刻要判断i<j
{
while (i<j && data[j]>x)j--;
if (i < j)
{
data[i] = data[j];
i++;
}
while (i < j && data[i] < x)i++;
if (i < j)
{
data[j] = data[i];
j--;
}
}//这个大循环进行完表示一次左右划分好了
data[i] = x;//此时i==j
quick_sort(data, l, i - 1);//左边再排一次
quick_sort(data, i + 1, r);//右边再排一次
}
return;//当传参的i==j时,说明已经排好了,无法再分着排了
}
int main()
{
int n; cin >> n;
int data[105];
for (int i = 1; i <= n; i++)
{
cin >> data[i];
}
quick_sort(data,1,n);//数列的第一个和最后一个
for (int i = 1; i <= n; i++)
{
cout << data[i] << " ";
}
return 0;
}
//基于选择的排序
//1.选择排序:
每次从待排序区中选择一个最小的数,放在待排序区中的第一个位置,
//重复以上过程,待排序区递减,有序区递增,直到排序完成。
//性质:就地的 不稳定的 内部的
时间复杂度:O(n^2)
#include<iostream>
using namespace std;
int main()//这个代码和冒泡一样,可以用flag进行优化
{
int n; cin >> n;
int data[105];
for (int i = 1; i <= n; i++)
{
cin >> data[i];
}
for (int i = 1; i <= n - 1; i++)
{
int minn = i;//记录一趟中最小元素的下标
for (int j = i + 1; j <= n; j++)
{
if (data[minn] > data[j])
minn = j;
}
//将每次最小的元素放在每趟无序区的一个的位置 i 处
swap(data[minn], data[i]);
}
for (int i = 1; i <= n; i++)
{
cout << data[i] << " ";
}
return 0;
}
//2.堆排序
// 基础概念:
//堆(Heap)是⼀类基于完全⼆叉树的特殊数据结构。通常将堆分为两种类型:
//1、⼤顶堆(Max Heap):在⼤顶堆中,根结点的值必须⼤于他的孩⼦结点的值,对于⼆叉树
//中所有⼦树都满⾜此规律
//2、⼩顶堆(Min Heap):在⼩顶堆中,根结点的值必须⼩于他的孩⼦结点的值,对于⼆叉树
//中所有⼦树都满⾜此规律
//思路:
//1.建小顶堆(两种方式)
// (1)自我初始化:先将原来的存到数组中,再从下到上,
// 先找只有一个或两个孩子结点的根结点(先找最小的有孩子结点的子树),
// 确定该子树的父亲结点比它的孩子节点小后,再向上判断调整更大子树的父亲节点
// 注意如果发生交换,还要判断交换后的下面的子树是否还符合上小下大的性质,判断到无孩子的结点为止
// 简述:自下而上判断,发生交换还要自上而下检查。
// (2)插入建堆:整一个空数组,从上到下插入数据,
// 插入同时,调整,使每次插入的数据小于其父亲节点的数据,如果大,就将其和父亲交换,
//每次交换后,从下往上查找,判断交换后会不会影响上面数据的平衡,一直到查找到根节点为止
//2.堆排序
//循环n次,每次输出最小的数
//每次输出根节点,根节点位置补上堆结尾的数据,再次堆排序
//以上过程循环n次
//性质:就地的 内部的 不稳定的
时间复杂度:O(nlogn)//建堆和排序时间复杂度都是这个
#include<iostream>
#include<cstdlib>
using namespace std;
void downheap(int* data, int i, int n)
{
int fa = i; int child;
while (fa * 2 <= n)
{
child = fa * 2;
if (child + 1 <= n && data[child + 1] < data[child])
{//找左右节点中最小的一个
child = child + 1;
}
if (data[child] < data[fa])
{//比较父亲节点和较小孩子节点的大小,通过选择,交换确定父亲为较小的那一个
swap(data[child], data[fa]);
fa = child;//这一步是让child作为根节点,
//再次循环,判断下面的子树是否因为上面结点的交换
// 而不满足小顶堆的条件
}
else
{
break;//要是fa结点才是三个节点中最小的
}//就没必要交换了,已经满足条件了
}
}
void insertheap(int* data, int i)
{
int child = i; int fa;//这里让child=i;
while (child > 1)
{
fa = child / 2;//思路和初始化的很像
if (data[fa] > data[child])
{
swap(data[fa], data[child]);
child = fa;
}
else
{
break;
}
}
}
int main()
{
int n; cin >> n;
int data[1005];
for (int i = 1; i <= n; i++)
{
cin >> data[i];
insertheap(data, i);
}
//根据完全二叉树的性质,n/2前的结点都是有孩子结点的根节点
//画一个完全二叉树试试就知道了
//for (int i = n / 2; i >= 1; i--)
//{//自我初始化小顶堆
// downheap(data, i, n);
//}
cout << "遍历小顶堆" << endl;
for (int i = 1; i <= n; i++)
cout << data[i] << " ";
//堆排序
cout << "\n" << "堆排序:" << endl;
int size = n;//实际长度
for (int i = 1; i <= n; i++)
{
cout << data[1] << " ";
data[1] = data[size]; //将头节点赋值为最后一个结点
size--; //删去最后一个结点
downheap(data, 1, size); //调整,建成小顶堆
}
return 0;
}
/*
实验数据:
8
53 17 78 9 45 65 87 32
小顶堆遍历输出:
9 17 65 32 45 78 87 53
堆排序输出:
9 17 32 45 53 65 78 87
*/
//基于插入的排序
(代码默认从1下标开始存数据)
//1.直接插入排序:插入时,遍历查找插入的位置,再插入(顺序表插入)
//-----析插入排序
//-----二路插入排序
//2.希尔排序
//1.直接插入排序
(从小到大排序)
// 逻辑:将数组分为有序的和无序的两部分,从前到后,一开始有序区只有一个元素,
// 每次将无序区的一个元素移入有序区,和有序区已存在的元素比较,如果有比它小的元素,
// 记录下标ind,和顺序表插入的思路一样,将其插入到ind位置
// 重复上两行的程序(n -1)次
//时间复杂度:O(n^2)
//性质:就地的 稳定的 内部的
#include<iostream>
using namespace std;
int main()
{
int data[105];
int n; cin >> n;
for (int i = 1; i <= n; i++)
{//输入数据
cin >> data[i];
}
int d, ind;//临时存数据 存确定位置下标
for (int i = 2; i <= n; i++)//第一次有序区只有一个数据,不用进行
{
d = data[i];
ind = i;
for (int j = 1; j < i; j++)//找出需要插入的下标ind
{
if (data[j] > d)
{
ind = j;
break; //记得加 前面可能有多个比d大的,取靠前的
}
}
for (int j = i; j > ind; j--)//将ind后的元素整体后移
{
data[j] = data[j - 1];
}
data[ind] = d;
}
for (int i = 1; i <= n; i++)
{
cout << data[i] << " ";
}
return 0;
}
/*
测试 数据:
6
3 1 7 5 2 4
*/
//改良后的直接插入排序:
(插入排序基本都用这个)
//不再先确定下标,再移动插入,而是从后往前和有序去对比
//前者大的话将前面的这个元素后移,不大的话就取消比较,确定位置下标,直接赋值
//上述两行运行n-2遍 (等于是把查找确定下标和插入两步合成了一步)
//时间复杂度:O(n^2)
//性质:就地的 稳定的 内部的
#include<iostream>
using namespace std;
int main()
{
int data[105];
int n; cin >> n;
for (int i = 1; i <= n; i++)
{//输入数据
cin >> data[i];
}
int d;//临时存数据
for (int i = 2; i <= n; i++)
{
d = data[i];
int j;//存确定位置下标
for (j = i; j >= 2; j--)
{
if (data[j - 1] > d)
data[j] = data[j - 1];
else
break;
}
data[j] = d;
}
/*for (int i = 2; i <= n; i++)
{//思路相同,代码第二种写法(注:dàt)
int j, t = data[i];
for ( j = i - 1; j >= 1; j--)
{
if (data[j] > t)
{
data[j + 1] = data[j];
}
else
break;
}
data[j+1] = t;
}*/
for (int i = 1; i <= n; i++)
{
cout << data[i] << " ";
}
return 0;
}
/*
测试 数据:
6
3 1 7 5 2 4
*/
// ---- - 析半插入排序:
将直接插入排序中查找确定插入位置ind的遍历查找部分,
// 优化为二分查找,for循环两个O(n)的循环,第一个O(n)变为了O(log2n),
// 但由于下面插入部分的循环仍为O(n),等于说是优化了个寂寞,总时间复杂度仍为O(n^2),
// 所以这里代码不展示。
//-----二路插入排序:
创建一个环形数组,先将从下标1部分·存进数组中,
//这里等于是将数组分为前后两部分,比下标1小的存它右边,比它小的存它左边,
//分为两部分多次进行如顺序表插入的操作,这样是将复杂度降低为原来的1/2,
//时间复杂度依旧是O(n^2),也是优化了个寂寞,这里代码就不展示了。
//2.希尔排序(shell)
(缩小增量排序):
//在直接插入排序基础上,对待排序数据进行分组,对每组进行排序,
//不断缩小组数并排序,最终缩小为1组
//每次分为(n/(2^m))组,m为分组次数 eg: n/2 n/4 n/8...(一种增量选择,这是希尔增量序列吗,还有别的选择,如hibbard增量序列)
//性质:就地的 不稳定的 内部的
时间复杂度:跟增量选择方式有关,希尔最差为O(n^2)
#include<iostream>
using namespace std;
int main()
{
int n; cin >> n;
int data[105];
for (int i = 1; i <= n; i++)
{
cin >> data[i];
}
int k = 0;//计算趟数
int t;
for (int d = n / 2; d >= 1; d /= 2)//遍历增量/组数
{
k++;//标记趟数
//以d为增量直接进行插入排序(代码把各组的插入排序放在了一起,更简便)
//这个就是变形的插入排序的代码
for (int i = d + 1; i <= n; i++)
{//这个插入在用的是上面改良部分第二种代码表示
t = data[i];
int j;
for (j = i-d; j >= 1; j-=d)
{
if (data[j] > t)
{
data[j+d] = data[j];
}
else
{
break;
}
}
data[j+d] = t;
}
cout << "第" << k << "趟 " << "增量为" << d << “ ”<<endl;
for (int i = 1; i <= n; i++)
{
cout << data[i] << " ";
}
cout<<endl;
}
return 0;
}
//基于归并的排序
//这里是2-路归并(简称归并排序)
//思路:先将数组不断二分,一直到变为n个单个元素,
// 再将其按照二分的路径逆着合并过来,合并的同时,遍历两组元素,
//将其按照从大到小的顺序排好,由此直到合并完成为止。
//代码思路:1.递归,输入左右下标 l , f ,不断二分,直到 l == f;
//2.创建一个临时数组t,将两组元素遍历按从小到大顺序存入t中,再将t的值赋给data数组
//性质:非就地的 稳定的 外部的
时间复杂度:O(nlogn)
#include<iostream>
#include<cstdlib>
using namespace std;
void merge(int* data, int l, int mid, int r)
{
int i = l;
int j = mid + 1;
int t[105], k = 0;//临时存储数据 控制该数组存入的下标
//将两组元素遍历按从小到大顺序存入t中
while (i <= mid && j <= r)
{//搞完之后,i , j有一个会越界,证明它所在的那半部分遍历完成
if (data[i] <= data[j])//注意,t数组是从下标0开始存的
{
t[k] = data[i];
k++;
i++;
}
else
{
t[k] = data[j];
k++;
j++;
}
}
while (i <= mid)
{//两个while循环,将另一半没遍历保存完的遍历完
t[k] = data[i];
k++;
i++;
}
while (j <= r)
{
t[k] = data[j];
k++;
j++;
}
for (int i = 0; i < k; i++)//将t中排好的数据赋给data
{
data[l + i] = t[i];
}
}
void merge_sort(int* data, int l, int r)
{//递归的思想
if (l < r)
{
int mid = (l + r) / 2;
//左半部分继续分
merge_sort(data, 1, mid);
//右半部分继续分
merge_sort(data, mid + 1, r);
//分完之后就和起来
merge(data, l, mid, r);
}
}
int main()
{
int n; cin >> n;
int data[105];
for (int i = 1; i <= n; i++)
{
cin >> data[i];
}
merge_sort(data, 1, n);
for (int i = 1; i <= n; i++)
cout << data[i] << " ";
return 0;
}
基于统计的排序
1.计数排序:
思路:统计数组中最大值到最小值之间所有数,每个数出现的次数,后直接按次数输出即可
eg : 原数组a[105],声明cut[...],注意全部初始化为0; ,将其中按照min -> max依次递增赋值(从下标min -> max赋值),
遍历执行cut[a[i]]++; 的程序,统计好个数,按顺序最后输出相应个数cut[a[i]]即可。
优点:时间复杂度O(n);
缺点:(1)空间换时间,(max-min)差太多时,浪费空间
(2)正常无法对负数和小数排序(负数要用偏移量改造,小数乘成整数)(记得改完最后还要改回来)
性质:内部的 非就地 稳定?->不一定(想要稳定加一个数组记录cut数组前缀和-->皮课1:00:00)
2.桶排序:
思路:将所有数'/10',根据结果将数组中元素分成若干组,再分别对每组进行以前学过的别的排序,后合并
分成若干组,每组有一个数组存储,这样一个数组可以看作一个桶。
时间复杂度:将数据分组->O(n)+后面桶内排序的时间复杂度
优缺点:比起后采取的排序,时间复杂度更低;比起计数排序,空间关键复杂度更低,算是一种居中和妥协。
3.基数排序:
将数组中所有的数据的位数补成和其中最高位的数一致,eg: 1->001 12->012 100->100
按照个位->十位->...的顺序进行计数排序,先对十位上的数字进行计数排序,依次到最高位
高位排序要使用低位排序的结果,eg: 021 025 十位排序一致,细分先后时需要根据个位排序021<025的结果将025排到021前面
直到排完最高位,得到结果。(思路讲解:皮课1:18:00)
性质:非就地 内部 稳定?->看计数排序怎么写
时间复杂度:O(d*(n+b)) d->最高位数 n->数组长度 b->进制数(eg : 十进制 b=10)