目录
一、排序介绍
排序是日常生活中频繁使用的功能,例如:淘宝的筛选功能,我们可以按照价格进行排序,销量进行排序,这样我们会更容易选择到优质的商品等。
二、排序分类
三、插入排序
3.1 直接插入排序
思想 :把第一个值当作排好序的,然后把第一个值与下一个值进行比较。
时间复杂度:最好情况(有序)O(N) 最坏情况O(N^2) 平均O(N^2)
后面的步骤也是一样的,一次一次调换位置,这里的end进入循环时,end位置前的值都是排序好的,插入排序一次只需要排好一个值,所以我们需要一个i来控制end的下一个位置。
#include<stdio.h>
void print(int* arr,int sz)
{
int i=0;
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
void Insert_sort(int* arr,int sz)
{
int i=0;
//用来控制比较
int end=0;
//sz-1的原因是我们里面采用的是arr[end]和arr[end+1],如果<sz的话就越界了
for(i=0;i<sz-1;i++)
{
end=i;
int tmp=arr[end+1];
while(end>=0)
{
if(arr[end]>tmp)
{
arr[end+1]=arr[end];
end--;
}
//如果在中途找到小的时候也是arr[end+1]=tmp,和越界是一样的,所以我们break
else
{
break;
}
}
arr[end+1]=tmp;
}
}
int main()
{
int arr[]={5,4,1,6,3,2};
Insert_sort(arr,6);
print(arr,6);
return 0;
}
3.2 希尔排序
思想:希尔排序是对插入排序的优化,插入排序在逆序的情况下表现是非常差的,我们要改进这一点就是尽量让它有序,我们可以设置一个间隔,在这个间隔内的让他有序,然后缩小间隔。
时间复杂度O(N*logN)
重点:gap=1的时候其实就是插入排序了,所以gap除以2,一直除到小于1就结束了
#include<stdio.h>
void print(int* arr,int sz)
{
int i=0;
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
void Shell_sort(int* arr,int sz)
{
int gap=sz/2;
int i=0;
//用来控制比较
int end=0;
//代码唯一最大的区别就在于gap,其他基本就是改一个值
while(gap>=1)
{
//看第一张图片,指向2的位置,2的位置就是最后一个,因为2的位置+gap就已经是最后一个了
for(i=0;i<sz-gap;i++)
{
end=i;
int tmp=arr[end+gap];
while(end>=0)
{
if(arr[end]>tmp)
{
arr[end+gap]=arr[end];
end=end-gap;
}
//如果在中途找到小的时候也是arr[end+1]=tmp,和越界是一样的,所以我们break
else
{
break;
}
}
arr[end+gap]=tmp;
}
gap=gap/2;
}
}
int main()
{
int arr[]={8,4,1,9,3,2,6,0,7};
Shell_sort(arr,9);
print(arr,9);
return 0;
}
四、选择排序
4.1 直接选择排序
思想:把第一个元素认为成是最小的,然后往下比较,有更小的,就把它的位置给min,找到最小的之后,和第一个元素进行交换,然后把第二个元素认为成最小的,继续比较...
时间复杂度:O(N^2) 这个排序是最差的排序,不推荐使用!
#include<stdio.h>
void print(int* arr,int sz)
{
int i=0;
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
void swap(int* p1,int *p2)
{
int tmp=*p1;
*p1=*p2;
*p2=tmp;
}
void select_sort(int* arr,int sz)
{
int i=0;
int j=0;
int min=0;
for(i=0;i<sz-1;i++)
{
min=i;
for(j=i+1;j<sz;j++)
{
//找出最小的值
if(arr[min]>arr[j])
{
min=j;
}
}
//如果最小的位置不是i,就交换
if(min!=i)
{
swap(&arr[i],&arr[min]);
}
}
}
int main()
{
int arr[]={8,4,1,9,3,2,6,0,7};
select_sort(arr,9);
print(arr,9);
return 0;
}
选择排序可以优化一下,我们可以一次找两个值,一个最大,一个最小。
#include<stdio.h>
void print(int* arr,int sz)
{
int i=0;
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
void swap(int* p1,int *p2)
{
int tmp=*p1;
*p1=*p2;
*p2=tmp;
}
void select_sort_optimize(int* arr,int sz)
{
int begin=0;
int end=sz-1;
//一次找2个,一个最大,一个最小
int max=0;
int min=0;
while(begin<end)
{
max=begin;
min=begin;
for(int j=begin;j<=end;j++)
{
if(arr[min]>arr[j])
{
min=j;
}
if(arr[max]<arr[j])
{
max=j;
}
}
swap(&arr[begin],&arr[min]);
if(max==begin)
{
max=min;
}
swap(&arr[end],&arr[max]);
begin++;
end--;
}
}
int main()
{
int arr[]={5,6,1,4,3,2,10,7,8};
int sz=sizeof(arr)/sizeof(arr[0]);
select_sort_optimize(arr,sz);
print(arr,sz);
return 0;
}
4.2 堆排序
思想:在逻辑结构上看成二叉树,所谓逻辑结构就是想象出来的,物理结构上是数组,这个排序较为复杂。
时间复杂度:O(N*logN)
这里我会把堆排序的每一步逐一拆解,整个流程和我走一遍后,在回过头来看,就会明白其中的用意。
堆排序就是把数组变成一个完全二叉树,首先假设我们有一个数组:
把3变成根节点,9、6是孩子节点,7、8是9的孩子节点,5、2是6的孩子节点,依此类推下去...
就形成了这样:
然后这里我们要了解一个概念
(1)大堆:每一个子树都满足双亲大于两个孩子 (2)小堆:每个子树都满足双亲小于孩子
了解这个概念后,问读者一个问题,这个是堆吗?明显不是,3下面的两个节点是大堆,但是整个二叉树并不是堆,因为3不大于9和6
所以我们需要进行一个操作,与下面的每一行进行比较,把最大的换过来,详细请看图:
这样不就是大堆了吗?在此之前,我们还需要了解一个概念,左孩子=2*父节点+1,即leftchild=2*parent+1,右孩子=2*父节点+2,即rightchild=2*parent+2,知道这些概念后,实现起来就非常容易了,代码如下:
void heapify(int* tree,int n,int root)
{
int parent=root;
int leftchild=2*parent+1;
int rightchild=2*parent+2;
//完全二叉树没有左节点一定也没有右节点,所以我们用左节点去判断
while(leftchild<n)
{
int max=parent;
//比较大小
if(leftchild<n&&tree[max]<tree[leftchild])
{
max=leftchild;
}
if(rightchild<n&&tree[rightchild]>tree[max])
{
max=rightchild;
}
//这里满足条件就交换
if(max!=parent)
{
swap(&tree[parent],&tree[max]);
parent=max;
leftchild=2*parent+1;
rightchild=2*parent+2;
}
//这里的意思是已经是大堆了,就直接退出了
else
{
break;
}
}
}
这里的heapify就是变成堆,应该不难理解吧?但是有一个问题,就是我们如何让根节点下面的节点都是大堆呢?
方法很简单我们只需要从最后一个开始建堆就可以了,假设我们有这样一个数组(图以二叉树的形式表示):
想解决这种问题,我们就要从最后一个(7)开始建堆,然后是0,不过这里我们并不需要从2,6,0,7,原因是他们没有孩子节点,所以我们需要从9开始建堆,如何找到9呢?
父节点=(孩子节点-1)/2
找到9以后,我们下标-1,这样又回到了8去建堆,回到了最初的开始,理解以后代码实现起来就非常容易了,代码如下:
void build_heap(int* tree,int n)
{
//最后一个孩子节点
int child=n-1;
//最后一个父节点
int parent=(child-1)/2;
int i=0;
for(i=parent;i>=0;i--)
{
heapify(tree,n,i);
}
}
这一步完成后,我们的二叉树就变成了这样:
前面的步骤完成了,就差最后一步堆排序了,那么我们该如何实现这个过程呢?
步骤:头节点和最后一个节点进行交换,最后一个节点就是最大的了,然后砍掉最后一个,然后进行heapify(进行heapify的目的就是再把最大的放到头节点),再把头节点和最后一个交换,依此类推...步骤如下图:
剩下的步骤就不写出来了,都是重复的步骤,我们直接看代码(这里把剩下的代码也放出来):
void print(int* arr,int sz)
{
int i=0;
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
void swap(int* p1,int *p2)
{
int tmp=*p1;
*p1=*p2;
*p2=tmp;
}
void heap_sort(int* tree,int n)
{
build_heap(tree,n);
int begin=0;
int end=n-1;
//end从最后一个走到第一个
while(end>=0)
{
swap(&tree[begin],&tree[end]);
heapify(tree,end,0);
end--;
}
}
int main()
{
int tree[]={8,4,10,9,3,2,6,0,7};
int n=sizeof(tree)/sizeof(tree[0]);
heap_sort(tree,n);
print(tree,n);
return 0;
}
五、交换排序
5.1 冒泡排序
思想:从左到右,相邻的元素进行交换,每次比较一轮就会有一个最大或者最小的元素。
时间复杂度:最差情况 O(N^2) 最好情况 O(N)平均情况O(N^2)
代码如下:
#include<stdio.h>
#include<stdlib.h>
void swap(int *p1,int* p2)
{
int tmp=*p1;
*p1=*p2;
*p2=tmp;
}
void bubble_sort(int* arr,int sz)
{
int flag=1;
int i=0;
int j=0;
for(i=0;i<sz-1;i++)
{
for(j=0;j<sz-1-i;j++)
{
if(arr[j]>arr[j+1])
{
swap(&arr[j],&arr[j+1]);
flag=0;
}
}
//优化
//如果第一次就是有序,直接退出,这样可以让最好情况在O(N)
if(flag==1)
{
break;
}
}
}
void print(int* arr,int sz)
{
int i=0;
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
int main()
{
int arr[]={1,2,3,4,5};
int sz=sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr,sz);
print(arr,sz);
return 0;
}
5.2 快速排序
快速排序是所有排序中最常用的一种排序
时间复杂度:O(N*logN)
5.2.1 挖坑法
有如下数组:
把数组第一个元素的位置定义为坑,并且把5关键值(后续用来比较),然后分别定义end(数组后一个位置),begin数组第一个位置
用arr[end]去和arr[key]比较大小,找到比arr[key]小的以后放到pviot的位置,然后再把pivot的位置换到end的位置上
再用arr[begin]和arr[key]比较大小,找到比arr[key]大的以后放到pivot的位置,然后再把pivot的位置换到begin的位置上
再用arr[end]去和arr[key]比较大小,找到比arr[key]小的以后放到pivot的位置,然后再把pivot的位置换到end的位置上
再用arr[begin]和arr[key]比较大小,找到比arr[key]大的以后放到pviot的位置,不过这是begin和end相遇了,所以我们需要结束,然后把arr[key]的值赋给arr[pivot]
这是我们发现5的左边都是小于5的元素,5的右边都是大于5的元素
总结:end去找小,begin去找大,找到了就放到pivot的位置,更新pivot的位置
这一步完成后,后面就是递归了,我们需要递归去拆解这个步骤。
左边第一次递归:
这一次左递归是没有改变的,因为没有值比0更小,所以继续用左区间递归,不过这里有左区间了吗?已经没有了,所以左递归结束了,需要右递归,就是4,1,2,3这几个值
递归的详细过程省略直接得第二次递归的答案:
这里需要继续左递归,即3,1,2:
直接得答案:
继续左递归,即2,1(这里直接给答案了):
然后我们需要右递归,发现右边是一个3,不需要递归,然后在右递归,发现右边是一个4,也不需要递归,最后返回去,我们整个的左递归就完成了,如下:
5的左边递归回来了,就递归5的右边,这里不再展示,思路一样的,实现如下:
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define MAX 100
void swap(int *p1,int* p2)
{
int tmp=*p1;
*p1=*p2;
*p2=tmp;
}
void quicksort(int* arr,int left,int right)
{
//递归结束条件,只剩下一个,或者left>right的时候停止
if(left>=right)
{
return;
}
int begin=left;
int end=right;
int key=arr[begin];
int pivot=begin;
//相遇就会停止
while(begin<end)
{
//end找小,这里begin<end不可以少,可能中途就会相等
//这里的等于号不要忘了,不然会造成死循环
while(begin<end&&arr[end]>=key)
{
end--;
}
arr[pivot]=arr[end];
pivot=end;
//begin找大
while(begin<end&&arr[begin]<=key)
{
begin++;
}
arr[pivot]=arr[begin];
pivot=begin;
}
arr[pivot]=key;
//区间[left,pivot-1] pivot [pivot+1,right]
quicksort(arr,left,pivot-1);
quicksort(arr,pivot+1,right);
}
void print(int* arr,int sz)
{
int i=0;
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
int main()
{
int arr[]={8,6,2,10,2,5,8,1,9,8,4,2};
int sz=sizeof(arr)/sizeof(arr[0]);
quicksort(arr,0,sz-1);
print(arr,sz);
return 0;
}
非递归
思想:利用栈的先进后出,可以做出和递归一样的效果。
//递归的缺陷:栈帧深度太深,栈空间不够用,可能会溢出
int partsort(int* arr,int left,int right)
{
int begin=left;
int end=right;
int key=arr[begin];
int pivot=begin;
//相遇就会停止
while(begin<end)
{
//end找小,这里begin<end不可以少,可能中途就会相等
//这里的等于号不要忘了,不然会造成死循环
while(begin<end&&arr[end]>=key)
{
end--;
}
arr[pivot]=arr[end];
pivot=end;
//begin找大
while(begin<end&&arr[begin]<=key)
{
begin++;
}
arr[pivot]=arr[begin];
pivot=begin;
}
arr[pivot]=key;
return pivot;
}
void quicksortNonR(int* arr,int sz)
{
ST s;
StackInit(&s);
StackPush(&s,sz-1);
StackPush(&s,0);
if(!StackEmpty(&s))
{
int left=StackTop(&s);
StackPop(&s);
int right=StackTop(&s);
StackPop(&s);
int Index=partsort(arr,left,right);
//[left,Index-1] Index [Index+1,right]
if(Index+1<right)
{
StackPush(&s,right);
StackPush(&s,Index+1);
}
if(left<Index-1)
{
StackPush(&s,Index-1);
StackPush(&s,left);
}
}
}
5.2.3 左右指针法
思想:end找小,begin找大,找到时,两个互相交换,直begin和end相遇,左右指针发和挖坑法基本类似。
//左右指针
void quicksort(int* arr,int left,int right)
{
if(left>=right)
{
return;
}
int begin=left;
int end=right;
int key=left;
while(begin<end)
{
//end找小
while(begin<end&&arr[end]>=arr[key])
{
end--;
}
//begin找大
while(begin<end&&arr[begin]<=arr[key])
{
begin++;
}
//大和小进行交换
swap(&arr[begin],&arr[end]);
}
swap(&arr[begin],&arr[key]);
//[left,begin] begin [begin+1,right]
quicksort(arr,left,begin-1);
quicksort(arr,begin+1,right);
}
5.2.4 前后指针法
思想:(这里用升序思维讲解)利用当前指针和key(第一个元素)进行比较,如果他比key小,prev指针+1,然后和当前指针交换。
假设有这样一个数组:
然后我们定义成这样:
arr[cur]和arr[key]比较大小,如果比他小就++prev,然后arr[cur]和arr[prev]交换:
cur继续往下走, arr[cur]和arr[key]比较大小,如果比他小就++prev,然后arr[cur]和arr[prev]交换:
cur继续往下走, arr[cur]和arr[key]比较大小,这次大于key,所以不用++prev:
cur继续往下走,依然大于key,所以依旧不需要++prev:
cur继续往下走,这次比较大小,我们发现arr[cur]<key,我们需要++prev,然后交换arr[prev]和arr[cur]:
cur继续往下走, arr[cur]<key,++prev,交换arr[prev]和arr[cur]:
cur继续往下走, arr[cur]<key,++prev,交换arr[prev]和arr[cur]:
cur继续走,走到10和8的时候,prev是不变的, 直到cur走出去,走出去后,我们只需要交换arr[prev]和key就可以了:
代码如下:
#include<stdio.h>
void swap(int* p1,int* p2)
{
int tmp=*p1;
*p1=*p2;
*p2=tmp;
}
void print(int* arr,int sz)
{
int i=0;
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
void quicksort(int* arr,int left,int right)
{
if(left>=right)
{
return;
}
int prev=left;
int key=left;
int cur=prev+1;
while(cur<=right)
{
if(arr[cur]<arr[key])
{
++prev;
//如果cur==prev就不交换了,因为没有意义
if(cur!=prev)
{
swap(&arr[cur],&arr[prev]);
}
}
++cur;
}
swap(&arr[prev],&arr[key]);
//[left,prev-1] prev [prev+1,right]
quicksort(arr,left,prev-1);
quicksort(arr,prev+1,right);
}
int main()
{
//5 1 2 3 4 6 9 7 10 8
int arr[]={6,1,2,7,9,3,4,5,10,8};
int sz=sizeof(arr)/sizeof(arr[0]);
quicksort(arr,0,sz-1);
print(arr,sz);
return 0;
}
六、归并排序
6.1 归并排序
思想:归并排序的核心思想是分治,我们需要将一个完整的数组先进行拆分,随后合并,合并可以一层一层把逆序变成顺序,和快速排序的思想有点类似,快速排序是每一层进行排序,然后拆分,归并排序则是拆分在排序。
时间复杂度:O(N*logN)
假设有这样一个数组:
另外我们在动态内存开辟一个临时数组,用来交换(将在代码里展示),这几步做好以后,我们要需要求出中间的位置,mid=2(left=0,right=5)
然后我们开始递归,left和mid递归,mid+1和right递归,继续求中间变量,直到只剩下一个为止(这里左区间先递归,所以只展示左区间):
左区间为[left,mid] 右区间为[mid+1,right], 继续递归,先递归左区间:
左区间为[left,mid],右区间为[mid+1,right],继续递归,这时发现left=right,左递归结束,右边也是left=right,右递归结束,这时我们需要用到临时数组了
然后在把5,6放到原数组的位置去(不展示),还记得我们这里递归回去还差一个右区间吗?还剩下一个1,这个1也是单独的数字,所以递归结束,然后进行比较:
在回到原数组:
左边已经有序了,还差右边,继续递归,然后整个数组的左区间和右区间进行比较就可以了,思想是一样的(不再展示),代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define MAX 10
void print(int* arr,int sz)
{
int i=0;
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
void _mergesort(int* arr,int* tmp,int left,int right)
{
if(left>=right)
{
return;
}
int mid=(left+right)/2;
//分解
//[left,mid] [mid+1,right]
_mergesort(arr,tmp,left,mid);
_mergesort(arr,tmp,mid+1,right);
int i=left;
int begin1=left;
int end1=mid;
int begin2=mid+1;
int end2=right;
while(begin1<=end1&&begin2<=end2)
{
if(arr[begin1]<arr[begin2])
{
tmp[i++]=arr[begin1++];
}
else
{
tmp[i++]=arr[begin2++];
}
}
//把剩下饿数组也放到tmp数里
while(begin1<=end1)
{
tmp[i++]=arr[begin1++];
}
while(begin2<=end2)
{
tmp[i++]=arr[begin2++];
}
//放到原数组
for(i=left;i<=right;i++)
{
arr[i]=tmp[i];
}
}
void merge_sort(int* arr,int sz)
{
//动态内存分配
int* tmp=(int*)malloc(sizeof(int)*sz);
_mergesort(arr,tmp,0,sz-1);
free(tmp);
tmp=NULL;
}
int main()
{
srand((unsigned)time(NULL));
// int arr[]={5,6,1,3,8,9};
// int sz=sizeof(arr)/sizeof(arr[0]);
// merge_sort(arr,sz);
// print(arr,sz);
int arr1[MAX];
for(int i=0;i<MAX;i++)
{
arr1[i]=rand()%100;
}
print(arr1,MAX);
merge_sort(arr1,MAX);
print(arr1,MAX);
return 0;
}
非递归
思想:利用循环从1个和1个比,到2个和2个比,然后到4个和4个比,就是递归的逆推过程,递归是一直分到最后一个开始比较,而循环则是一上来就从1个开始比较。
非递归的精髓在于:定义的gap以及退出的条件,下面是代码:
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define MAX 10
void swap(int* p1,int* p2)
{
int tmp=*p1;
*p1=*p2;
*p2=tmp;
}
void print(int* arr,int sz)
{
int i=0;
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
void merge_sort(int* arr,int sz)
{
int* tmp=(int*)malloc(sizeof(int)*sz);
int gap=1;
int i=0;
int begin1=0;
int end2=0;
while(gap<sz)
{
for(i=0;i<sz;i+=gap*2)
{
//[i,i+gap-1] [i+gap,i+2*gap-1]
begin1=i;
int end1=i+gap-1;
int begin2=i+gap;
end2=i+2*gap-1;
//记录tmp下次开始的位置
int j=begin1;
//右半区间不存在
if(begin2>sz)
{
break;
}
//右半区间不全,需要修正
if(end2>sz)
{
end2=sz-1;
}
while(begin1<=end1&&begin2<=end2)
{
if(arr[begin1]>arr[begin2])
{
tmp[j++]=arr[begin2++];
}
else
{
tmp[j++]=arr[begin1++];
}
}
while(begin1<=end1)
{
tmp[j++]=arr[begin1++];
}
while(begin2<=end2)
{
tmp[j++]=arr[begin2++];
}
for(int k=i;k<=end2;k++)
{
arr[k]=tmp[k];
}
}
gap*=2;
}
}
int main()
{
srand((unsigned)time(NULL));
int arr1[MAX];
for(int i=0;i<MAX;i++)
{
arr1[i]=rand()%100;
}
print(arr1,MAX);
merge_sort(arr1,MAX);
print(arr1,MAX);
//5 1 2 3 4 6 9 7 10 8
// int arr[]={6,1,2,7,9,3,4,5,10,8};
// int sz=sizeof(arr)/sizeof(arr[0]);
// merge_sort(arr,sz);
// print(arr,sz);
return 0;
}
如有不对希望读者指正!