在学习计算机科学的过程中,我一直觉得有一点奇怪,因为大部分教科书和高中有一点很大的差别,就是关于计算机学史的内容寥寥无几,大部分算法学习过程可能是按复杂度从高到低,或者相似的思想一起学习,而不是像大部分学科一样,按学科发展的流程去发展。所以我一直有一点奇怪这是为什么,所以在我详细的学习完十大基础排序算法后想要去按时间顺序整理一份排序。可是在收集资料的时候我才发现我面临最大的问题,恰恰就是没有资料。除了希尔排序,其他排序算法都没有署名,也没有发明日期。这一计划失败后我个人猜测最主要的原因可能是在于计算机出现的时间短,发展太过迅速,这些基础的算法发明者大部分不屑于在这种简单的算法上,就连希尔也说过根据Metzner本人的说法,“我没有为这种算法做任何事,我的名字不应该出现在算法的名字中。”所以我还是按照学习顺序来对这些算法总结。
这种排序方法是我所见到的第一种排序方法,它的意义在与从第一个数开始和后面每一个数比较,找到属于他的位置。
时间复杂度为o[N²] 是一种简单的排序算法
代码如下
#include
int main()
{
int a[1000];
int n;
scanf("%d",&n)
for(int i=0;i<n-1;i++)
for(int j=i+1;j<n-1-i;j++)
{
if(a[j]>a[j+1])
{
int c=a[j+1];
a[j+1]=a[j];
a[j]=c;
}
return 0
}
在学习冒泡之后选择排序几乎是同时进入我的视线,甚至在一段时间我把选择排序当成冒泡使用。所以其实相比于冒泡排序我个人更喜欢选择排序,应为他的逻辑更加清晰。先选出所有数理最大的,在和他应该在的位置交换
复杂度同样是O[N²]
代码如下
#include
int main()
{
int a[1000];
int n,max=-100;
scanf("%d",&n)
for(int i=0;i<n;i++)
{
max=0;// 初始化
for(int j=0;j<n-i;j++)
{
if(a[j]>max)
{
max=a[j];
temp=j;
}
}
a[temp]=a[n-i-1];
a[n-i-1]=max;
}
return 0;}
对于插入排序,实际上我是比较晚才开始了解的,也几乎没有任何使用经验。但很多人认为这是最好理解的排序算法,所以我把它放在第三位。
时间复杂度也是O[N²]
代码如下
for(int i=1;i<=n;i++)
{
j=i-1;
target=a[i];
while(j>-1&&target<a[j])
{
a[j+1]=a[j];
j--;
}
a[j+1]=target;
}
第一次写可能会磕磕绊绊。关键的几个边界问题要处理好,他和冒泡最大的区别在于,插入尽可能多比较少交换。交换次数更少,找到合适的位置才进行交换工作。
提前吧要换的位置空出,并存储,如果向前就先将其后移。直到最后j+1和j+2所指向的位置都是一样的,吧我们选定的数组中的数放入j+1即可
在此提供一种递归做法
#include
void sort11(int *a,int n)
{
if(n=0)return;
sortll(a[],n-1);
int i=n-1;
int q=a[n];
while(i>=0&&q<a[i])
{
a[i+1]=a[i];
i--;
}a[i+1]=q;
}
for(int i=n-2;i>=0;i--)
{
j=i+1;
target=a[i];
while(target>a[j]&&j<=n)
{
a[j-1]=a[j];
j++;
}
a[j-1]=target;
}
本质上是先拍好n位,在把第n+1位选择合适的位置插入。
可以体会一下第一种和第三种的区别。
说了插入,不可避免的要谈到希尔排序,
对于希尔排序说白了就是先分组在排序,组距渐渐变小,一次次的排序中会让数组变得越来越有序,减少后续交换次数,是插入排序一种优化方式
关于分组一般从n/2渐渐降到1,
很多人认为他在插入排序的基础上进行了多余的工作。但本质上他是为了减少以后每次排序的工作量
其代码只需在插入排序基础上做一点点修改即可
for(int w=n/2;w>0;w=w/2)
{
for(int i=1;i<=n;i++)
{
j=i-w;
target=a[i];
while(j>-1&&target<a[j])
{
a[j+w]=a[j];
j=j-w;
}
a[j+w]=target;
}
}
关于为什么第二层循环i只是++需要注意
排序中并不必要优先排完每一组,按顺序进行,多组并列也可并且注意边界限制条件一定是j>-1而不能是j!=-1
上面四种排序原理不同,但时间复杂度都是n²,这一复杂度很多时候不能满足我们的要求。所以我们要想办法去改进排序算法
在改进是有人提出分治法解决问题
先从中间分开数组,在将两组数合并。至于合并工作就需要开辟新空间,比较二个数组中最小的进入新数组,在选择之后最小的玩成合并功能
这一算法很容易用递归实现。只需设好边界即可解决问题
它的复杂度只有O[N*lgN]
void _Merge(int *a, int begin1, int end1, int begin2, int end2, int *tmp)
{
int index = begin1;
int i = begin1, j = begin2;
while (i <= end1&&j <= end2){
if (a[i]<=a[j])
tmp[index++] = a[i++];
else
tmp[index++] = a[j++];
}
while (i <= end1)
tmp[index++] = a[i++];
while (j <= end2)
tmp[index++] = a[j++];
memcpy(a + begin1, tmp + begin1, sizeof(int)*(end2 - begin1 + 1);
}
void MergeSort(int *a, int left, int right, int *tmp)
{
if (left >= right)
return;
int mid = left + ((right - left) >> 1);
MergeSort(a, left, mid, tmp);
MergeSort(a, mid + 1, right, tmp);
_Merge(a, left, mid, mid + 1, right, tmp);
}
tmp为辅助数组。分为两部分完成实际上merge也可直接在排序中完成。为了让程序更条理,我没有选择这么去写
应该来说,在现在无论是是任何工作,快速排序的使用率都是非常高的
它的复杂度也只有O[N*lgN]
事实上,快排也用了分治的思想,先把问题分割
只不过快排并不是直接从中间分隔,而是选择一个合适的中值。然后把比它小的放一边,比他大的放另一边。这是一种非常有效的想法因为他很好的解决了合并数组的繁杂操作
至于如何选择中值,大部分时候,我们会采用三点中值法,它会选择数组中第一个最后一个还有中间一个,选择他们中的中位数,再将这个数和数组首位交换。而分为两半的过程中,我们常常选择从前后同时扫描选择前面第一个大于目标值的数,和后面第一个小于目标的数,在进行交换,当两个扫描针交错或者错过时就找到了目标值应该位于的位置,需要注意的是,目标值应该和从后像前扫的交换,因为他会停留在一个小于目标值的数,(右边都大于)这样我们就完成了原理的分析下面是代码化的实现
void swap(int a[],int i,int j)
{
int temp=a[i];
a[i]=a[j];
a[j]=temp;
}
void Qsort(int *a,int b,int c)
{
if(b>=c)return;
else{
int left=b,right=c;
int mid=left-(right-left)/2;
if (a[left]>a[mid]);
swap(a,left,mid);
if (a[left]>a[right]);
swap(a,left,right);
if a([mid]>a[right]);
swap(a,mid,right);
swap(a,mid,left);//保证mid一定是中值
}
int key=a[b];
left=left+1;
while(left<right)
{
while(a[left]<key&&left<rihgt)
left++;
while(a[right]>key&&left<rihgt)
right--;
swap(a,left,right);
} swap(a,right,b);
Qsort(a,b,right-1);
Qsort(a,right+1;c);
}
堆排序是一种很有趣的排序方式,它用了二叉树,通过调整堆完成排序。本质上来说它是选择排序的一种强化版本
他把选择排序的遍历过程变成了只查找一般。运用堆的性质完成了对一个数组的排序
提到堆,我们就要先了解二叉树的一些基本性质如果根是i它的左枝应该是2i+1右枝是2n+2,
堆必须是完全二叉树。分为大顶堆和小顶堆。性质是根一定大于两个叶子或小于
(小心2*i+1越界不是所有根都有枝)
关于复杂度的问题,我们只是把选择部分优化为了lgn所以总的复杂度应该是O[N/*LGN]
下面放一段参考代码
void max_heapify(int arr[], int start, int end)
{
int dad = start;
int son = dad * 2 + 1;
while (son <= end) //若子节点指标在范围内才做比较
{
if (son + 1 <= end && arr[son] < arr[son + 1])
son++;
if (arr[dad] > arr[son]) //如果父节点大於子节点代表调整完毕,直接跳出函数
return;
else //否则交换父子内容再继续子节点和孙节点比较
swap(arr,dad,son);
}
}
void heap_sort(int arr[], int len)
{
int i;
//初始化,保证数组目前是堆
for (i = len / 2 - 1; i >= 0; i--)
max_heapify(arr, i, len - 1);
//先将第一个元素和已排好元素前一位做交换,再重新调整,直到排序完毕
for (i = len - 1; i > 0; i--)
{
swap(arr,0,i);
max_heapify(arr, 0, i - 1);
}
}
这种排序方法的复杂度只有O[N]但是为了达到线性复杂度,他牺牲了一定的空间
需要开一个大小为k+1的数组,如果n很小k很大的情况下,会大大浪费空间。必要时可以选择,它的原理就是遍历原数组,把原数组中的每一个数出现的次数存到新开辟的数组中。存完后再反向释放出来,知道存储量为零是停下
代码如下
#include
int main()
{
int max=0;int p=0;
scanf("%d",&n)
int a[10000];
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
for(int i=0;i<n;i++)
if(a[i]>max)max=a[i];
int b[max+1]={
0};
for(int j=0;j<n;j++)
b[a[j]]++;
for(int i=1;i<=max;i++)
while(b[i]>0)
{
b[i]--;
a[p++]=i;
}
for(int w=0;w<n;w++)
printf("%d ",a[w]);
return 0;
}
桶排序是基于计数排序
#include
#include
typedef struct node{
int key;
struct node* next;
}KeyNode;
void bucket_sort(int keys[],int size,int bucket_size);
int main()
{
int a[]={
11,11,9,21,14,55,77,99,53,25};
int size=sizeof(a)/sizeof(a[0]);
bucket_sort(a,size,10);
return 0;
}
void bucket_sort(int keys[],int size,int bucket_size)
{
KeyNode **bucket_table=(KeyNode**)malloc(bucket_size*sizeof(KeyNode*));
for(int i=0;i<bucket_size;i++){
//初始化桶
bucket_table[i]=(KeyNode*)malloc(sizeof(KeyNode));
bucket_table[i]->key=0;
bucket_table[i]->next=NULL;
}
for(int i=0;i<size;i++){
KeyNode* node=(KeyNode*)malloc(sizeof(KeyNode));
node->key=keys[i];
node->next=NULL;
int index=keys[i]/10;//给数据分类的方法(关系到排序速度,很重要)
KeyNode *p=bucket_table[index];
if(p->key==0){
p->next=node;
p->key++;
}
else{
while(p->next!=NULL&&p->next->key<=node->key){
//=的时候后来的元素会排在后面
p=p->next;
}
node->next=p->next;
p->next=node;
(bucket_table[index]->key)++;
}
}
KeyNode* k=NULL;
for(int i=0;i<bucket_size;i++){
for(k=bucket_table[i]->next;k!=NULL;k=k->next){
printf("%d ",k->key);
}
}
}
先排个位在排侍卫。。。最后就按照通吐出来
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
int d = 1; //保存最大的位数
int p = 10;
for(int i = 0; i < n; ++i)
{
while(data[i] >= p)
{
p *= 10;
++d;
}
}
return d;
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int *tmp = newint[n];
int *count = newint[10]; //计数器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进行d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete[]tmp;
delete[]count;
}
十种简单排序就到此为止了,在自己写的过程中,发现了一些以前没有发现的小问题,所以有时候不能自信认为自己都会了。多实践才能提高能力