写一写常见的排序算法的思想,包括 冒泡排序、选择排序、插入排序、shell排序、归并排序、快速排序、堆排序
冒泡排序
- 思路:通过不停的交换相邻元素.每一次循环都把最后未排序的元素里面最大的移动到后面去了.每次循环后, 未排序的元素就会减少1.
// 通过交换相邻的两个元素达到排序的目的
// 每次循环相当于把最大的一个放到后面
void my_maopao(int* arr,int len){
if (len < 2) {
return;
}
// 外层循环相当于控制次数
for (int i = 0; i < len - 1; ++i) {
// 内层循环是不停的比较交换相邻的两个数
for (int j = 0; j < len - 1 - i; ++j) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
}
简单验证下正确性:
void test_rank(){
for (int i = 0; i < 100; ++i) {
// 产生2组数据
vector<int> arr1;
vector<int> arr2;
int count = rand() % 1000 + 1000;
for (int j = 0; j < count; ++j) {
int num = rand() % count;
arr1.push_back(num);
arr2.push_back(num);
}
// 用我们实现的算法对 arr1 进行排序
// 用 stl 的 sort 对 arr2 进行排序
// 然后验证结果是否正确
my_maopao(arr1.data(),(int)arr1.size());
sort(arr2.begin(),arr2.end());
for (int j = 0; j < arr1.size(); ++j) {
assert(arr1[j] == arr2[j]);
}
}
printf("测试通过.
");
}
选择排序
- 思路: 每次都从未排序的数组中选择一个最大的,然后放到合适的位置.
// Q:其实这是选择排序
// 思想就是,每次从未排序的里面选择一个最大的 放到后面
void my_select_sort(int* arr,int len){
if (len < 2) {
return;
}
// 升序排列
// 外面的循环控制次数
for (int i = 0; i < len - 1; ++i) {
int maxIndex = 0;
// 内循环每次找出最大的值,然后放到合适的位置
for (int j = 0; j < len - i; ++j) {
if (arr[j] > arr[maxIndex]) {
maxIndex = j;
}
}
// 将 maxIndex 放到正确的位置
if (maxIndex != len - i - 1) {
int temp = arr[maxIndex];
arr[maxIndex] = arr[len - i - 1];
arr[len - i - 1] = temp;
}
}
}
验证方法同上.
插入排序
- 思想: 每次拿一个数字出来,插入到前面已经排序的序列中.
// 插入排序
void my_insert_sort(int* arr,int len){
if (len < 2) {
return;
}
// 从索引为1开始,依次插入
// 表示 i 前面的是已经排好序的
for (int i = 1; i < len; ++i) {
// 应该是从后往前比较
// 从已经排好序的里面找一个比 arr[i]要小的,然后放到其后面
// 如果没有找到,则放到索引为0的位置
for (int j = i-1; j >=0; --j) {
if (arr[j] > arr[ j + 1]) {
// 交换
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}else{
continue;
}
}
}
};
shell排序
- 思想:先按照一个增量(间隔)进行分组,然后对每个分组进行插入排序,然后减小增量,继续对分组进行插入排序. 要求增量一定能减小到1,进行一次插入排序.
- 和插入排序的区别: 当需要排序的数组比较大时,插入排序时,可能会需要进行比较大量的移动.shell排序进行分组排序的目的就是减少插入时的移动.
void my_shell_sort(int* arr,int len){
if (len < 2) {
return;
}
// 选取合适的增量,需要保证 N 可以缩减到 N = 1
// 一般取质数,N = len
// 我们这里取 N = N/3 + 1
// 假如len = 20,则开始 N = 7
// 分组情况为:
// a[0],a[7],a[14]
// a[1],a[8],a[15]
// a[2],a[9],a[16]
// a[3],a[10],a[17]
// a[4],a[11],a[18]
// a[5],a[12],a[19]
// a[6],a[13]
// 第一次循环就是分别对这些分组进行插入排序
int N = len;
// 让增量N缩减到1
while (N > 1) {
N = N / 13 + 1;
// 对分组进行循环,对 a[N]到a[len-1] 进行分别插入
for (int i = N; i < len; ++i) {
// j 表示各个分组里面的前一个元素,各个分组前面的元素都是已经排好序的
// j -= N 表示 a[j]所在分组的前一个元素
for (int j = i - N; j >= 0; j -= N) {
if (arr[j] > arr[j + N]) {
// 交换
int temp = arr[j];
arr[j] = arr[j + N];
arr[j + N] = temp;
}else{
continue;
}
}
}
}
}
说明 N = N/13 + 1,选择增量最好是选择质数.
归并排序
- 思想:将数组分为左右两部分,对两部分进行归并排序,最后合并.分治思想的典型例子.
// 将两个已经排序的数组合并起来
// start <= mid <= end
// 表示 arr[start,mid] arr[mid+1,end] 都已经排好序
void my_meger_1(int* arr,int start,int mid,int end){
if (start >= end) {
return;
}
int lenL = mid - start + 1;
int lenR = end - (mid + 1) + 1;
int L[lenL];
int R[lenR];
for (int i = 0, j = start; i < lenL; ++i,++j) {
L[i] = arr[j];
}
for (int i = 0, j = mid + 1; i < lenR ; ++i,++j) {
R[i] = arr[j];
}
// 一个一个的放入数组
int i = 0;
int j = 0;
int idx = start;
while (i < lenL && j < lenR) {
if (L[i] > R[j]) {
arr[idx] = R[j];
++j;
++idx;
}else{
arr[idx] = L[i];
++i;
++idx;
}
}
// 有可能有一边还没处理
if (i == lenL) {
for (; j < lenR; ++j) {
arr[idx] = R[j];
++idx;
}
}else{
for (; i < lenL; ++i) {
arr[idx] = L[i];
++idx;
}
}
}
// 对 arr 的 [start,end]
void recursive_meger_sort_1(int* arr,int start,int end){
if (start == end) {
return;
}
int mid = (start + end) / 2;
recursive_meger_sort_1(arr,start,mid);
recursive_meger_sort_1(arr,mid + 1, end);
my_meger_1(arr,start,mid,end);
}
// 归并排序
// 这里使用,先对数组进行递归分组
void my_meger_sort_1(int* arr,int len){
if (len < 2) {
return;
}
recursive_meger_sort_1(arr,0,len - 1);
}
快速排序
- 思想: 和归并类似,也是分治思想. 从数组中选择一个元素A,根据此元素将数组分成左右两部分,左边都小于A,右边都大于等于A,然后对左右进行递归.
int my_partition(int* arr,int start,int end){
// 这里我们简单的取 arr 的第一个元素作为分割的标准数
int key = arr[start];
// arr[start,end]范围内比 key 小的都放在左边,比 key 大的都放在右边
// 可以这样思考,有几个比 key 小,则 key 的索引就是几
int len = end - start + 1;
int keyIndex = start;
for (int i = 1; i < len; ++i) {
if (arr[start + i] < key) {
// 需要将 arr[start + i]移动到key当前位置的前面
int temp = arr[start + i];
for (int j = start + i - 1; j >= keyIndex; --j) {
arr[j + 1] = arr[j];
}
arr[keyIndex] = temp;
++keyIndex;
}
}
return keyIndex;
}
void my_quick_recursion(int* arr,int start,int end){
// 递归的结束条件
if (start >= end) {
return;
}
int index = my_partition(arr,start,end);
my_quick_recursion(arr,start,index - 1);
my_quick_recursion(arr,index + 1,end);
}
// 快速排序
void my_quick_sort(int* arr,int len){
if (len < 2) {
return;
}
my_quick_recursion(arr,0,len-1);
}
堆排序
- 思想:利用二叉堆数据结构的性质.将数组构建成最大堆,然后不停的从最大堆中获取最大元素放到合适的位置.
主要要进行几个操作:- 保持最大堆的性质 a[i] >= a[left[i]] && a[i] >= a[right[i]]
- 二叉堆是一个完全二叉树,满足完全二叉树的性质.如根节点索引为i,左子树节点 2i + 1,右子树节点 2i + 2.
// 将 arr idx 索引为根的树保持最大堆的性质
// 要求 arr[idx]的左子树和右子树都是最大堆
void keep_heap(int* arr,int idx,int len){
if (idx >= len) {
return;
}
int leftIndex = 2 * idx + 1;
int rightIndex = 2 * idx + 2;
if (leftIndex >= len) {
// 左子树不存在,说明是叶子节点
return;
}
if (rightIndex >= len) {
// 右子树不存在,只需要和 a[left[i]] 比较即可
if (arr[idx] < arr[leftIndex]) {
int temp = arr[idx];
arr[idx] = arr[leftIndex];
arr[leftIndex] = temp;
}
return;
}
// 左子树和右子树都存在
int largestIndex = idx;
if (arr[idx] < arr[leftIndex]) {
largestIndex = leftIndex;
}
if (arr[rightIndex] > arr[largestIndex]) {
largestIndex = rightIndex;
}
if (largestIndex != idx) {
// 根节点比子节点小,需要将节点 arr[idx] arr[leftIndex] arr[rightIndex] 中最大的提上来
int temp = arr[idx];
arr[idx] = arr[largestIndex];
arr[largestIndex] = temp;
// 原来的 arr[idx] 已经下降,但是下降后,可能比其下降后的左子树/右子树小,导致需要继续下降
keep_heap(arr,largestIndex,len);
}
}
void my_make_heap(int* arr,int len){
int startIndex = len / 2;
for (int i = startIndex; i >= 0; --i) {
keep_heap(arr,i,len);
}
}
void my_heap_sort(int* arr,int len){
if (len < 2) {
return;
}
// 1.根据 arr 创建 一个最大堆
my_make_heap(arr,len);
// 2.进行排序,原理是每次交换根元素和最后一个元素,然后将最后一个元素从堆中排除
int lastIndex = len - 1;
while (lastIndex >= 1) {
int temp = arr[lastIndex];
arr[lastIndex] = arr[0];
arr[0] = temp;
--lastIndex;
keep_heap(arr,0,lastIndex + 1);
}
}
总结:
对于上面的排序,
冒泡、选择、插入、快速 是稳定的排序.
shell、归并、快速、堆 是不稳定的排序.
主要是学习思想,上面可以优化的地方有很多,实际使用还是使用 stl 的 sort(不稳定) 和 stable_sort(稳定).