后台开发学习笔记(一、排序算法上)

最近在准备系统学习一下后台基础,做为程序员(假的),编程基础还是很重要的,目前在跟着king老师学习后台开发,在学习当中,通过写博客的方式,把学到的知识整理和归纳。也给未来的自己留个回忆吧。如果有写的不对的地方,欢迎指正,毕竟是初学者。

1.1 排序介绍

排序算法有很多,比较基础的有下列10种:

  • 冒泡排序
  • 选择排序
  • 插入排序
  • 希尔排序
  • 归并排序
  • 快速排序
  • 堆排序
  • 计数排序
  • 桶排序
  • 基数排序

还有一些比较脑洞大开的,比如鸡尾酒排序、猴子排序、睡眠排序。

此外,排序算法还可以根据其稳定性,划分为稳定排序不稳定排序
即如果值相同的元素在排序后仍然保存着排序前的顺序,则这样的排序算法是稳定排序;如果值相同的元素在排序后打乱了排序前的顺序,这样的排序算法是不稳定排序。例如下面的例子:
在这里插入图片描述
图片内容来自《漫画算法》

根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为内排序外排序。(我都不是很了解外排序,不在内存中的意思)

1.2 冒泡排序

冒泡排序思想,我们要把相邻的元素两两比较,当一个元素大于右侧相邻元素时,交换他们的位置(要把大的元素移动到右则),当一个元素小于右则相邻元素时,不动(大的元素在右边)。下面详细过程:
在这里插入图片描述
上面是冒泡排序的基本思路,一共需要两个for循环,内循环把较大的数移动到右侧,外循环是一共有8个数,都要循环一边才能把8个数排序,下面是代码部分:

 /**
  * @brief  冒泡排序1
  * @param  base: 数据的基地址
  *         len : 数据的长度
  * @retval 成功返回0,失败返回负数
  */
int bubble_sort1(int *base, int len)
{
    int i = 0 , j = 0;
    int temp;
    for(i=0; i<len-1; i++)   //外循环,一共循环n-1
    {
        for(j=0; j<len-i-1; j++)	//内循环,一共循环n-i-1
        {
           if(base[j] > base[j+1])
           {
               temp = base[j];
               base[j] = base[j+1];
               base[j+1] = temp;
           }
        }
    }

    return 0;
}

这一个冒泡排序是按步就班的排,没有做优化,下面有冒泡排序的优化版:
在这里插入图片描述
如上图,可以看出只需要一次循环即可,如果用bubble_sort1()就还是要循环(n-1)*(n-i-1)次,这样明显性能不好,所以需要添加一个标志位,如果数据在一个循环中,判断已经是有序的了,就不需要再循环了,代码如下:

/**
  * @brief  冒泡排序2
  * @param  base: 数据的基地址
  *         len : 数据的长度
  * @retval 成功返回0,失败返回负数
  */
int bubble_sort2(int *base, int len)
{
    int i = 0 , j = 0;
    int temp;
    int flag = 1;       //1是有序的
    for(i=0; i<len-1; i++)
    {
        flag = 1;       //初始默认是有序的
        for(j=0; j<len-i-1; j++)   //进入循环
        {
           if(base[j] > base[j+1])
           {
               flag = 0;                //这次循环无序的,标志置0
               temp = base[j];
               base[j] = base[j+1];
               base[j+1] = temp;
           }
        }
        if(flag == 1)               //如果是有序的就退出
            break;
    }

    return 0;
}

冒泡排序是不是优化到这里就为止了呢?其实不是,下面还有一个例子可以继续优化:
在这里插入图片描述
图片来自《漫画算法》
由上图可以发现,其实右边的数据已经排好序了,然后冒泡算法还是要继续循环排序,性能不好,所以可以记录一下无序数列的边界,再往后就有序了,这个无序数列边界就是每一轮之后交换的那个数,就是无序数列边界了,代码如下:

/**
  * @brief  冒泡排序3,在2的基础上,优化了右侧有序就不用循环了
  * @param  base: 数据的基地址
  *         len : 数据的长度
  * @retval 成功返回0,失败返回负数
  */
int bubble_sort3(int *base, int len)
{
    int i = 0 , j = 0;
    int k = 0;
    int temp;
    int flag = 1;       //1是有序的
    int sort_border = len-1;
    int sort_border_temp = sort_border;
    for(i=0; i<len-1; i++)
    {
        flag = 1;       //初始默认是有序的
        sort_border_temp = sort_border;  //每次循环完一次之后,要更新下次循环的次数
        for(j=0; j<sort_border_temp; j++)   //进入循环,循环判断条件
        {
           if(base[j] > base[j+1])
           {
               flag = 0;                //这次循环无序的,标志置0
               temp = base[j];
               base[j] = base[j+1];
               base[j+1] = temp;
               sort_border = j;
               
           }
        }
         printf("sort_border %d %d\n", sort_border, j);
        if(flag == 1)               //如果是有序的就退出
            break;
    }

    return 0;
}

经过了两次优化了冒泡算法,在有些时候效果是提升了。不过还真不容易啊,冒泡排序还有这么多事情可以搞。

1.3 选择排序

选择排序的基本思想是,遍历一遍查找数列里面最小的值,然后跟第一个值交换,然后继续第二轮遍历,查找第二小的值,然后跟第二个值交换,一直这样继续操作,整体遍历完数列就已经排好序了。下面来个图示:
在这里插入图片描述
选择排序也需要两个for循环,外for循环n-1次,负责把数据放好,内for循环n-i-1次,负责寻找到档次最小的值,然后进行交换,代码如下:

/**
  * @brief  选择排序
  * @param  base: 数据的基地址
  *         len : 数据的长度
  * @retval 成功返回0,失败返回负数
  */
int select_sort(int *base, int len)
{
    int i = 0 , j = 0;
    int small_index = 0;
    int temp = 0;

    for(i=0; i<len-1; i++)
    {
        //small = base[i];            //每次循环,获取当前i上的值,作为默认最小值
        small_index = i;
        for(j=i+1; j<len; j++)        //进入循环,循环判断条件  因为 i已经在移动了,所以j<len就这样
        {
           if(base[j] < base[small_index])
           {
               //small = base[j];
               small_index = j;
           }
            printf("sort_border %d %d\n", small_index, j);
        }
       
        if(i != small_index)
        {
            temp = base[i];
            base[i] = base[small_index];
            base[small_index] = temp;             
        }
    }

    return 0;
}

选择排序的好处就是交换少,这样也是提升性能的一种方法。

1.4 插入排序

插入排序,就想是我们玩扑克牌的时候,进行理牌,刚摸到一张牌,就找好这张牌的位置,然后插入。例如:
在这里插入图片描述
第一次摸到5,因为只有一个,所以不排序,第二次摸到8,8比5大,所以排在5的右边,第三次摸到6,6比8小,所以需要插入,然后6又比5大,所以插入到5和8之间,第四次摸到3,因为3比5,6,8都少,所以插入到5的左边,以此类推,即可完成排序。

但是在程序中,不能直接插入,需要申请一个临时变量,保存刚摸到的牌,然后拿刚摸到的牌和已经排好序的牌比较,如果刚摸的牌小,拿原来的牌往后移动一位,预留出空间,一直查到比刚摸的牌小的数字,然后就插入到这里位置。即可完成代码。

/**
  * @brief  插入排序
  * @param  base: 数据的基地址
  *         len : 数据的长度
  * @retval 成功返回0,失败返回负数
  */
int insert_sort(int *base, int len)
{
    int i=0, j = 0;
    int temp = 0;
    for(i=1; i<=len-1; i++)
    {      
        temp = base[i];     //刚摸的牌,存储到临时变量
        for ( j=i; temp<base[j-1]; j-- )   //拿到刚摸的牌进行循环判断
        {
            printf("i= %d %d %d\n", i, base[j], base[j-1]);
            base[j] = base[j-1];	//刚摸的牌小,那就顺序往后挪一位
        }

        if(j != i)   
        {
            base[j] = temp;   //对比完成后,找到合适的位置插入
        }
    }

    return 0;
}

1.5 希尔排序

这个希尔排序确实很难理解,我也是搞了很久才理解了一下下,记录一下理解的过程,哈哈。
希尔排序是根据某个增量,来分割成各个子序列,然后在子序列中进行比较和交换,依次循环,还是先上图吧,文字还难描述:
在这里插入图片描述
希尔排序的步骤,就是如上图所示,这里我选择的增量是len/2=4,所以第一轮比较是分为
4组,子序列分别是{5,9},{8,2},{4,1}和{3,7}。然后在这4个子序列中分别排序小的在左,分别是{5,9},**{2,8},{1,4}**和{3,7}。这时数组的序列变成了{5,2,1,3,9,8,4,7};接下来第二轮,这次选择的增量是4/2=2,上一个增量除以2,这次分为2组,增量为2,子序列分别为:{5,1,9,4}和{2,3,8,7},然后两个子序列分别排序,排序后变成:{1,4,5,9}和{2,3,7,8},原数组中的序列变成:{1,2,4,3,5,7,9,8},再次选择增量2/2=1,这次就分为1组,相当于每一个都比较,其实在这时候序列基本有序了,所以就把基本有序变成有序了。
接下来代码:

/**
  * @brief  希尔排序
  * @param  base: 数据的基地址
  *         len : 数据的长度
  * @retval 成功返回0,失败返回负数
  */
int shell_sort(int *base, int len)
{
    int i=0, j = 0;
    int temp = 0;
    int gap = len;                //增量

    //增量的定义可能不同,我们这里去/2
    for ( gap = len/2; gap >= 1; gap /= 2)  //这个就是增量每次都除以2
    {
        for ( i = gap; i < len; i++)  //这个就是循环移动哨兵
        {       
            temp = base[i];		//临时存储起来
            printf("gap = %d i = %d j = %d base[i] = %d base[j] = %d\n", gap, i, i-gap, base[i], base[i-gap]);   //分析打印
            for(j=i-gap; j>=0 && temp<base[j]; j-=gap)
//上面就是在哨兵变化的时候,j跟着变,每次都是比哨兵-gap,这样就形成了分组对比
//temp<base[j]  就行进行比较,如果temp小的话,就要跟前面的交换,
//j-=gap 这个是因为在一个组里,会有几个元素,所以需要往前把前面的元素也对比了
            {
                printf("for gap = %d i = %d j = %d base[i] = %d base[j] = %d\n", gap, j+gap, j, base[j+gap], base[j]);   //分析打印
                base[j+gap] = base[j];     
//temp比前面那个元素小,所以把前面的元素填充到j+gap的位置。这个需要自己跟据打印分析
            }
            base[j+gap] = temp;   //把temp填充到他自己的位置
            bianli(base);	//这个也是分析打印的,是打印真的数组的元素
        }
    }

    return 0;
}

下面把程序运行的打印粘贴出来,再分析分析,确实不好理解:

# 第一轮分组,增量为4
gap = 4 i = 4 j = 0 base[i] = 9 base[j] = 5  #  下标  4 0 比较
5 8 4 3 9 2 1 7 
gap = 4 i = 5 j = 1 base[i] = 2 base[j] = 8  #  下标  5 1 比较  
for gap = 4 i = 5 j = 1 base[i] = 2 base[j] = 8	#temp小进行交换
5 2 4 3 9 8 1 7 
gap = 4 i = 6 j = 2 base[i] = 1 base[j] = 4  #  下标  6 2 比较  
for gap = 4 i = 6 j = 2 base[i] = 1 base[j] = 4  #temp小进行交换
5 2 1 3 9 8 4 7 
gap = 4 i = 7 j = 3 base[i] = 7 base[j] = 3	 #  下标  7 3 比较
5 2 1 3 9 8 4 7 
# 第二轮分组,增量4/2=2
gap = 2 i = 2 j = 0 base[i] = 1 base[j] = 5   #  下标  2 0 比较
for gap = 2 i = 2 j = 0 base[i] = 1 base[j] = 5  #temp小进行交换
1 2 5 3 9 8 4 7 
gap = 2 i = 3 j = 1 base[i] = 3 base[j] = 2  #  下标  3 1 比较
1 2 5 3 9 8 4 7 
gap = 2 i = 4 j = 2 base[i] = 9 base[j] = 5  #  下标  4 2 比较
1 2 5 3 9 8 4 7 
gap = 2 i = 5 j = 3 base[i] = 8 base[j] = 3  #  下标  5 3 比较
1 2 5 3 9 8 4 7 
gap = 2 i = 6 j = 4 base[i] = 4 base[j] = 9  #  下标  6 4 比较
for gap = 2 i = 6 j = 4 base[i] = 4 base[j] = 9  #temp小进行交换
for gap = 2 i = 4 j = 2 base[i] = 9 base[j] = 5  #temp现在是再4,结果比前面的2还小,需要继续交换,这时候赋值的j+gap就很巧妙,下面差不多了,就不分析了
1 2 4 3 5 8 9 7 
gap = 2 i = 7 j = 5 base[i] = 7 base[j] = 8
for gap = 2 i = 7 j = 5 base[i] = 7 base[j] = 8
1 2 4 3 5 7 9 8 
gap = 1 i = 1 j = 0 base[i] = 2 base[j] = 1
1 2 4 3 5 7 9 8 
gap = 1 i = 2 j = 1 base[i] = 4 base[j] = 2
1 2 4 3 5 7 9 8 
gap = 1 i = 3 j = 2 base[i] = 3 base[j] = 4
for gap = 1 i = 3 j = 2 base[i] = 3 base[j] = 4
1 2 3 4 5 7 9 8 
gap = 1 i = 4 j = 3 base[i] = 5 base[j] = 4
1 2 3 4 5 7 9 8 
gap = 1 i = 5 j = 4 base[i] = 7 base[j] = 5
1 2 3 4 5 7 9 8 
gap = 1 i = 6 j = 5 base[i] = 9 base[j] = 7
1 2 3 4 5 7 9 8 
gap = 1 i = 7 j = 6 base[i] = 8 base[j] = 9
for gap = 1 i = 7 j = 6 base[i] = 8 base[j] = 9
1 2 3 4 5 7 8 9 

1.6 归并排序

归并排序是利用归并的思想实现的排序方法。不多说 ,先上图:
在这里插入图片描述
归并排序就是先把整个序列进行拆分,直到拆分成每个元素一组的时候,上图第一轮到第三轮的时候就是进行拆分的,拆分完成之后,就可以进行归并排序了,从第四轮开始,相邻的两组进行归并,并且排序,所以就形成了第四轮的样子,归并成了4组,然后继续归并,这次归并成了两组,变成了第五轮,然后继续归并成一组,就排完序,这次的图描述的很形象了,不需要在用语言描述了,下面来看代码:

/**
  * @brief  归并
  * @param  base: 数据的基地址
  *         len : 数据的长度
  * @retval 成功返回0,失败返回负数
  */
static int merge(int *base, int *temp, int start, int middle, int end)
//需要归并的数组,这个数组是有2个,一个是start -> middle  一个是middle+1 -> end
{
    int i = 0, j = 0,k = start;
    
    printf("merge %d %d %d\n", start, middle, end);
    //归并
    for(i=start, j=middle+1; i<=middle && j<=end; k++)  //i=0, j=1;   
    //两个数组的下标同时移位,并且判断那个比较小,小的存储到temp这个临时数组中
    {
        if(base[i] < base[j])       //如果是i的小,就保存到temp中
        {
            temp[k] = base[i++];
        }
        else						//如果是j的小,也保存到temp中
        {
            temp[k] = base[j++];
        }
    }

    while(i<=middle)    //有可能一个j的数组比较小,都拷贝完了,i的数组还没拷贝完,所以需要继续拷贝
    {
        temp[k++] = base[i++];
    }

    while(j<=end)		//有可能一个i的数组比较小,都拷贝完了,j的数组还没拷贝完,所以需要继续拷贝
    {
        temp[k++] = base[j++];
    }

    //需要拷贝回到data中,这里是借助了临时变量temp,但是数据还是要拷贝到base里,返回的
    for(i=start; i<=end; i++)
    {
        base[i] = temp[i];
    }

    bianli(base);
    return 0;
}

/**
  * @brief  归并排序
  * @param  base: 数据的基地址
  *         len : 数据的长度
  * @retval 成功返回0,失败返回负数
  */
int merge_sort(int *base, int *temp, int start, int end)   //因为是递归代码,所以需要每次传参的时候,要传开始和结束下标
{
    //start开始下标,end结束下标
    int middle; 
    printf("start %d %d\n", start, end);
    if(start < end)      //判断递归结束条件,这个就相当于一个元素一组的时候了
    {
        middle = start + (end-start)/2;   //计算中间middle的值
        printf("middle %d %d %d\n", start, middle, end);    //分析打印
        merge_sort(base, temp, start, middle);  // 0 3  0 1    0 0   //开始递归  左边的注释就是递归的start和middle的值
        printf("end %d %d %d\n", start, middle+1, end);    //分析打印
        merge_sort(base, temp, middle+1, end);   //  4 7  2 3   1 1  //开始递归  左边的注释就是递归的middle+1和end

        //排序,倒序归并排序
        printf(">>>>>>>>>>>>>>>>\n");
        merge(base, temp, start, middle, end);   //进行归并并排序
    }
    printf("返回 \n");
    return 0;
}

代码分析就这么多了,这种递归的代码确实比较难理解,可以通过程序打印来继续分析:

# 分析递归,还是先抓住返回的时候,返回的时候我已经打印了
start 0 7          # 开始传参,我们数组下标范围就是  0  -- 7
middle 0 3 7          # 进行拆分
start 0 3               # 前半段(1前),进行递归,所以范围只有  0 -- 3
middle 0 1 3
start 0 1					# 前半段的前半段在进行递归,(2前) 这次的范围更小了  0  -- 1
middle 0 0 1
start 0 0						# 前半段的前半段的前半段(3前) 在进行递归,范围是 0 -- 0 因为这次是一个元素了,所以递归返回
返回 
end 0 1 1                       # 返回到前半段的前半段的后半段进行递归(3后)  范围是 1 -- 1
start 1 1						#这次也是一个元素了,所以递归返回
返回 
>>>>>>>>>>>>>>>>                # 第一次进行归并排序
merge 0 0 1						#  排序的范围是 0 - 1  ,也就是2前,(对应的图是5,8)
5 8 4 3 9 2 1 7 
返回 						
end 0 2 3					#返回前半段的后半段(2后),这次的范围是2 -- 3
start 2 3						
middle 2 2 3					#前半段的后半段的前半段开始递归 ,范围是 2 -- 2 
start 2 2
返回 
end 2 3 3					    #前半段的后半段的后半段开始递归 ,范围是 3 -- 3 
start 3 3
返回 
>>>>>>>>>>>>>>>>
merge 2 2 3						# 开始归并,对应的图的 (4,3)
5 8 3 4 9 2 1 7 
返回 
>>>>>>>>>>>>>>>>			# 前半段开始归并 范围 0 -- 3  对应的是(5,8,3,4)
merge 0 1 3
3 4 5 8 9 2 1 7 
返回 
end 0 4 7				
start 4 7				# 开始后半段的递归了  4 -- 7
middle 4 5 7				# 后半段的前半段开始递归  4 -- 5
start 4 5
middle 4 4 5					# 后半段的前半段的前半段, 4 -- 4 
start 4 4
返回 
end 4 5 5						# 后半段的前半段的后半段, 5 -- 5
start 5 5
返回 
>>>>>>>>>>>>>>>>				
merge 4 4 5						# 递归返回,并归并排序,对应图的(2,9)
3 4 5 8 2 9 1 7 
返回 
end 4 6 7					# 后半段的后半段开始递归  6 -- 7
start 6 7
middle 6 6 7					# 后半段的后半段的前半段 6 -- 6
start 6 6
返回 
end 6 7 7						# 后半段的后半段的后半段 7 -- 7
start 7 7
返回 
>>>>>>>>>>>>>>>>
merge 6 6 7						# 递归完成,并归并排序,对应图(1,7)
3 4 5 8 2 9 1 7 
返回 
>>>>>>>>>>>>>>>>			# 后半段开始归并,对应的图(2,9,1,7)
merge 4 5 7
3 4 5 8 1 2 7 9 
返回 
>>>>>>>>>>>>>>>>		# 最后两组归并并排序 对应着(3,4,5,8  1,2,7,9)
merge 0 3 7
1 2 3 4 5 7 8 9 
返回

这个递归确实有点难理解,不过慢慢分析还是可以的。

时间复杂度以后再分析补充,现在要赶进度,并且目前也没有系统学习过时间复杂度的计算,等之后看过《算法导论》之后,再补充,这一篇先记录5种排序算法,下一篇把另外5种补齐,就一个排序算法都搞的这么累,路还很长,且行且珍惜。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我知道了,下面是我的回答: 聚类算法是一种无监督学习算法,它可以将数据集中的数据按照某种规则进行分组,使得同一组内的数据相似度较高,不同组之间的数据相似度较低。聚类算法可以帮助我们对数据进行分类、分析和理解,同时也可以为其他机器学习任务提供数据预处理和特征提取的支持。 聚类算法的基本流程包括:确定聚类算法的目标函数、选择相似度度量方法、选择聚类算法、确定聚类的数量、进行聚类操作以及评估聚类效果。常见的聚类算法包括K-Means算法、层次聚类算法、密度聚类算法等。 K-Means算法是一种基于距离的聚类算法,它的基本思想是将数据集中的数据划分为K个簇,使得同一簇内的数据相似度较高,不同簇之间的数据相似度较低。K-Means算法的优点是计算复杂度较低,容易实现,但是需要预先指定簇的数量和初始聚类中心。 层次聚类算法是一种基于相似度的聚类算法,它的基本思想是不断合并数据集中相似度最高的数据,直到所有数据都被合并为一个簇或达到预先设定的簇的数量。层次聚类算法的优点是不需要预先指定簇的数量和初始聚类中心,但是计算复杂度较高。 密度聚类算法是一种基于密度的聚类算法,它的基本思想是将数据集中的数据划分为若干个密度相连的簇,不同簇之间的密度差距较大。密度聚类算法的优点是可以发现任意形状的簇,但是对于不同密度的簇分割效果不佳。 以上是聚类算法的基础知识,希望能对您有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值