排序算法心得:
如何阅读这篇博文:
我最近学习了有关排序的算法,有了不少感悟,所以打算写一篇博文记录一下,每一种算法我都会先介绍其核心思想,然后会给出相关代码,并在代码中写出相关的细节,你们在看着篇博文时建议先理解其核心思想,然后结合代码理解该算法
代码块里真的有亿点点细节要仔细看!!!
1.直接插入排序(思路简单的排序):
核心思想:将待排序序列的第一个元素与有序序列的元素进行对比找到合适的位置,并将该元素加到有序序列中去,然后再取无序序列中的第一个元素重复上述步骤,注意先前的那个元素已经加入到有序序列中去了;
代码如下及相关细节:
#include <stdio.h>
#define Keydef int
//创建一个自定义结构体
typedef struct {
Keydef Key;
}Datatype;
//直接插入排序核心代码
void InsertSort(Datatype a[], int n) {
int i, j;
Datatype temp;
for (i = 0;i < n - 1;i++) {//最外层循环是需要进行比较的次数,为什么是i<n-1?因为后面的代码是使a[i+1]与前面比较,避免数组越界
temp = a[i + 1];//使用temp记录无序序列中的第一个元素
j = i;//i前面的序列都已经是有序的,并且每完成一次循环,有序序列中的元素加1,这与i是相对应的
while (j > -1 && a[j].Key > temp.Key) {//将未排序的第一个元素与前面的元素比较,并在有序序列中寻找合适的位置
a[j + 1] = a[j];//为即将插入的元素空出位置,因为temp = a[i + 1];已经保存下来了所以可以直接覆盖
j--;
}
a[j + 1] = temp;//插入元素
}
}
void main() {
int i;
Datatype a[5] = {3,1,5,2,4};
InsertSort(a, 5);
for (i = 0;i < 5;i++) {
printf("%d ",a[i].Key);
}
}
代码结果:
直接插入排序的时间复杂度:O(n^2) 与数列的有序程度有关
直接插入排序的空间复杂度:O (1)
稳定性:稳定
2.希尔排序:(重点在理解最外面的两重循环)
核心思想:希尔排序是建立在直接插入排序的基础上的,我们知道直接插入排序的时间复杂度是与数列的有序程度有关,所以我们可以将待排序的数组分为几个小组,对每个小组都进行直接插入排序,然后减少小组的数目,再对每个小组进行直接插入排序,当完成了所有元素都在一个小组内的排序时,排序结束,因为对最后一个小组排序时小组是部分有序的所以时间复杂度优化了
代码如下及相关细节:
#include <stdio.h>
#define Keydef int
//创建一个自定义结构体
typedef struct {
Keydef Key;
}Datatype;
//直接插入排序核心代码
void ShellSort(Datatype a[], int n, int d[], int nd) {
int i, j,m,k,swap;
Datatype temp;
//最外层循环是对不同分组下的序列进行排序
for (m = 0; m < nd; m++) {
swap = d[m];//获取小组的数目
//第2层循环是对每的小组都进行排序
for (k = 0;k <swap;k++) {
//里面的双重循环是直接插入排序,只不过是将1改为swap
for (i = 0;i < n - swap;i=i+swap) {
temp = a[i + swap];
j = i;
while (j > -1 && a[j].Key > temp.Key) {
a[j + swap] = a[j];
j=j- swap;
}
a[j + swap] = temp;
}
}
}
}
void main() {
int i;
Datatype a[6] = { 3,1,5,2,4,6};
int d[3] = {3,2,1};
ShellSort(a, 6,d,3);
for (i = 0;i < 6;i++) {
printf("%d ", a[i].Key);
}
}
代码结果:
直接插入排序的时间复杂度:O(n(lbn)^2)
直接插入排序的空间复杂度:O (1)
稳定性:不稳定
3.直接选择排序
核心思想:将每个元素依次与其后的所有元素进行比较,每次都取出剩余元素的最小值或者最大值(这个算法学过c语言的都应该很了解,这里不多说)
代码如下及相关细节:
#include <stdio.h>
#define Keydef int
//创建一个自定义结构体
typedef struct {
Keydef Key;
}Datatype;
void SelectSort(Datatype a[],int n) {
int i, j,swap;
Datatype temp;
for (i = 0;i < n-1;i++) {
swap = i;
for (j = i + 1;j < n;j++) {
if (a[swap].Key < a[j].Key) {
swap = j;
}
temp = a[i];
a[i] = a[swap];
a[swap] = temp;
}
}
}
void main() {
int i;
Datatype a[6] = { 3,1,5,2,4,6 };
SelectSort(a, 6);
for (i = 0;i < 6;i++) {
printf("%d ", a[i].Key);
}
}
代码运行结果:
直接插入排序的时间复杂度:O(n^2)
直接插入排序的空间复杂度:O (1)
稳定性:不稳定但可做到稳定
4.堆排序:(该排序思路简单但代码相对繁琐)
核心思想:我决定直接用最通俗的语言描述它:堆排序就是不断重复以下步骤:构造最大堆或最小堆,然后将最大堆或者最小堆的根元素与最后一个元素交换(最大堆和最小堆的定义这里不详说不会的上网查)
所以堆排序的关键就是:
(1)构造最大堆,最小堆的思想和代码
(2)如何实现根元素与最后一个元素交换
下面先谈谈构造最大堆的思想:(最小堆也一样)
1.将数组元素按照完全二叉树的性质将其构造成为一颗完全二叉树
2.从最后一个非叶子节点开始调整使其满足堆的性质(根节点元素的值大于其孩子节点),将所有的非叶子节点都调整一遍,注意如果某个节点在调整后被其他节点所影响,序堆该节点重新调整
代码如下及相关细节:
#include <stdio.h>
#define Keydef int
//创建一个自定义结构体
typedef struct {
Keydef Key;
}Datatype;
void CreatHeap(Datatype a[],int n ,int h) {
int i = h;
Datatype temp = a[i];//记录下当前节点的信息
int j = 2 * i + 1;//记录其左孩子节点的位置
//创建一个判断变量flag,判断是否已经满足最大根堆的条件
int flag = 0;
//将根节点与其孩子节点进行比较,判断是否需要交换位置
while (j < n && flag != 1) {//为什么要使用循环因为要对被交换的节点也进行判断
//找出其左右孩子的较大者
if (j<n-1&&a[j + 1].Key > a[j].Key) j++;
if (temp.Key > a[j].Key) {//如果根节点已经大于其孩子节点则不用进行交换,设置flag=1,退出while循环
flag = 1;
}
else {//如果孩子节点大于根节点则需要交换孩子节点与根节点的位置,并且需要对被影响的节点再次进行判断
a[i] = a[j];
i = j;
j = 2 * i + 1;
}
a[i] = temp;
}
}
void InitateHeap(Datatype a[], int n) {//将待排序数组构成最大根堆
int i;
for (i = (n - 2)/2;i >= 0;i--) {//i记录的是叶子结点的根节点,从后往前
CreatHeap(a, n, i);
}
}
//利用最大根堆进行排序,不断将最大堆或者最小堆的根元素与最后一个元素交换
void HeapSort(Datatype a[], int n) {
int i;
Datatype temp;
InitateHeap(a, n);
for (i = n-1;i >0;i--) {
temp = a[0] ;//将堆顶元素与最后一个元素进行交换
a[0] = a[i];
a[i] = temp;
CreatHeap(a, i, 0);//在剩余的元素中重新构造最大堆
}
}
void main() {
int i;
Datatype a[6] = { 3,1,5,2,4,6 };
HeapSort(a,6);
for (i = 0;i < 6;i++) {
printf("%d ", a[i].Key);
}
}
直接插入排序的时间复杂度:O(n*lbn)
直接插入排序的空间复杂度:O (1)
稳定性:不稳定
冒泡排序:
核心思想:相邻比较大数后移
代码如下及相关细节:
#include <stdio.h>
#define Keydef int
//创建一个自定义结构体
typedef struct {
Keydef Key;
}Datatype;
void BubbleSort(Datatype a[],int n) {
int i, j;
Datatype temp;
for (i = 0;i < n;i++) {
for (j = 0;j < n-i-1;j++) {
if (a[j + 1].Key > a[j].Key) {
temp = a[j];
a[j ] = a[j+1];
a[j + 1] = temp;
}
}
}
}
void main() {
int i;
Datatype a[6] = { 3,1,5,2,4,6 };
BubbleSort(a, 6);
for (i = 0;i < 6;i++) {
printf("%d ", a[i].Key);
}
}
代码运行结果:
直接插入排序的时间复杂度:O(n^2)
直接插入排序的空间复杂度:O (1)
稳定性:稳定
快速排序
核心思想:选取数组中的某一元素作为基准(一般取数组第1个元素)。比该基准大的放在一边(递增序列放在右边,反之),比该基准小的放在另一边,这样就得到了一个,在基准的任意一边的元素都比该基准大或都小,然后再对基准的两边重复上述的步骤,直到基准的任意边只有它自己,或者说他的两边没有元素了(显然这是一个递归的过程,并且上面所说的就是递归的出口)。
注意该算法的难度就在与如何将比基准的的放在一边,比基准小的放在一边,对于这个问题我们一般是设置left,right.并同过这两个指针的交替运行实行数据的分开,注意如果选取了左端的元素为基准。则需从右边的指针开始分元素;
代码如下及相关细节:
#include <stdio.h>
#define Keydef int
//创建一个自定义结构体
typedef struct {
Keydef Key;
}Datatype;
void QuickSort(Datatype a[], int low, int hight) {//low指针指向要进行快排序列的最左端,hight指针指向要进行快排序列的最右端
int left=low;
int right=hight;
Datatype temp=a[low];//取最左端的元素为基准
while (left < right) {
//因为选取了左端的元素为基准,所以排序从右端开始,why?我这里很难解释,你认真看看代码应该能理解其中的妙处
//并且从右端寻找的是比基准要小的元素
while (left < right&&a[right].Key >= temp.Key) {
right--;
}
//如果找到了比基准小的元素则将元素放到基准的左边
if (left < right) {
a[left] = a[right];//这里为什么可以直接覆盖,因为刚开始时,a[left]为基准,且a[left]所指向的元素已经存放在了temp中
left++;//将指针后移方便后面的比较
}
//右指针完成了一次交换,则执行左指针,交替运行,为什么?因为这样可以有效的利用左端和右端空出来的一个数组空间。例如:
//初始时令temp=a[low],则a[low]数组元素的空间即可用于存放右端比较时第一个发现的比基准小的元素,当这个元素放在a[low]中存放好后
//其原本的空间右可以用来存出左端比基准大的元素,一次类推
while (left < right && a[left].Key < temp.Key) {
left++;
}
//如果找到了比基准小的元素则将元素放到基准的左边
if (left < right) {
a[right] = a[left];//这里为什么可以直接覆盖,因为刚开始时,a[left]为基准,且a[left]所指向的元素已经存放在了temp中
right++;
}
}
//当跳出最外层循环说明left=right这是我们将基准赋值给他们其中一个即可
a[left] = temp;
//下面是使用递归对基准的左边和右边再次进行快排,注意low和hight的范围
if(low<left) QuickSort(a, low, left-1);//对基准的左边进行快排
if (hight>right) QuickSort(a, right+1, hight);//对基准的左边进行快排
}
void main() {
int i;
Datatype a[6] = { 3,1,5,2,4,6 };
QuickSort(a, 0,5);
for (i = 0;i < 6;i++) {
printf("%d ", a[i].Key);
}
}
代码运行结果:
直接插入排序的时间复杂度:O(n*lbn)
直接插入排序的空间复杂度:O (n)
稳定性:不稳定
归并排序:(这个排序有点难度,建议多看几遍代码)
归并排序的核心思想:
1.首先将序列中需要排序的数字分为若干组,每组一个元素
2.将若干个组两两合并,合并过程中要保证合并后的组的有序性
3.重复第2步骤。直到只剩一个数组排序完成
上面思想中最为关键的就是合并两个组时,保证合并后的组的有序性:问题就在于如何保证:
我们不断取需要合并的两数组中第一个元素相比较,将较小的元素放进一个临时数组中保存,直到所有元素都放进临时数组中,注意如果比较过程中某个数组数据已经没有了,则直接将另一个数组中的元素按序加入临时数组中即可;
#include <stdio.h>
#include <stdlib.h>
#define Keydef int
//创建一个自定义结构体
typedef struct {
Keydef Key;
}Datatype;
void Merge(Datatype a[], int n, Datatype swap[], int k) {//注意k是需要归并的两数组的长度
int m = 0;//方便将元素放进临时变量
int u1, l1;//归并时第一个数组的上界和下界
int u2, l2;//归并时第二个数组的上界和下界
int i, j;
l1 = 0;//第一个数组初始时下界应为0;
while (l1 + k <= n-1) {//因为l2+k为归并时第二个数组的下界,如果l2+k存在则将两数组进行比较,否则直接将第一个数组加入临时数组即可
l2 = l1 + k;//归并时第二个数组的下界
u1 = l2 - 1;//第一个数组的上界
u2 = (l2 + k - 1 <= n - 1) ? l2 + k - 1 : n - 1;
//下面开始将两个数组的第一个元素进行比较并合并
for (i = l1, j = l2;i <= u1&&j <= u2;m++) {
if (a[i].Key <= a[j].Key) {//取两数组的头元素进行比较,将较小的加入临时数组中
swap[m] = a[i];
i++;//该变头元素的指向
}
else {
swap[m] = a[j];
j++;
}
}
while (i <=u1) {
swap[m] = a[i];
i++;
m++;
}
while (j <=u2) {
swap[m] = a[j];
j++;
m++;
}
l1 = u2 + 1;//归并完头两个数组后,继续归并其后的两个数组
}
//只有一组数组直接将其加入临时数组
for (i = l1;i < n;i++, m++) swap[m] = a[i];
}
void MergeSort(Datatype a[],int n) {
int i;
int k = 1;//k变量表示归并数组的长度,刚开始时归并长度为1
Datatype* swap = (Datatype*)malloc(sizeof(Datatype)*n);//创建一个动态数组来存储比较后的元素
while (k<n) {//当最后所有元素都在同一个数组时结束归并
Merge(a,n,swap,k);//归并的核心代码
for (i = 0;i < n;i++) a[i] = swap[i];//将临时数组的排序,放在数组a中方便下一次归并
k = 2 * k;//归并长度加倍
}
free (swap);
}
void main() {
int i;
Datatype a[6] = { 3,1,5,2,4,6 };
MergeSort(a, 6);
for (i = 0;i < 6;i++) {
printf("%d ", a[i].Key);
}
}
代码运行结果:
直接插入排序的时间复杂度:O(n*lbn)
直接插入排序的空间复杂度:O (n)
稳定性:稳定