二分查找法、选择排序法、递归算法

数据结构与算法

源代码地址:GitHub - firstsaofan/Data-structure-and-algorithm at develop

如果觉得样式不好:跳转即可 (md文件复制过来有些样式会不一样)

原文地址:https://lifengying.site/archives/er-fen-cha-zhao-fa--xuan-ze-pai-xu-fa--di-gui-suan-fa

二分法

例子:经典场景 猜数字游戏

1-100我心里指定一个数字,你来猜这个数字,

1.可以1------100一个一个猜

2.你可以直接从50 来猜 然后 25 13 7 4 2 1

代码

 //二分查找 查找次数O(logn)
 //猜数字游戏  
 Console.WriteLine("输入一个数字,代表1----N的范围");
 var n = Console.ReadLine();
 //输入的数字字符转化成整型
 var max = StringToNum(n);
 int start = 0;
 int end = max - 1;
 //电脑随机在1---n之间生成一个随机数,让用户猜
 Random ran = new Random();
 var item = ran.Next(1, max);
 Console.WriteLine(@$"输入1-----{max}之间的您猜的整数");
 int count = 0;
 while (start <= end)
 {
     //元素中间索引
 
     var mid = (start + end) / 2;//这个是自动向下取整的
     
     Console.WriteLine(@$"自动猜测的数字是{mid}");
     count++;
     if (mid == item)
    {
         Console.WriteLine("恭喜您,猜对了!!!");
         Console.WriteLine("游戏结束!!!");
         Console.WriteLine(@$"一共猜了{count}次");
         break;
    }
     else if (mid < item)
    {
         Console.WriteLine("数字猜小了!!!");
         //猜的数字小了 ,则正确的数字应该在右边 ---------------mid----------------
         //左边的所有元素全部排除则
         start = mid + 1;
    }
     else if (mid > item)
    {
         Console.WriteLine("数字猜大了!!!");
         //猜的数字小了 ,则正确的数字应该在左边 ---------------mid----------------
         //右边的所有元素全部排除则
         end = mid - 1;
    }
 
 }
 
 //Console.WriteLine("Hello, World!");
 
  static int StringToNum(string n)
 {
     return Convert.ToInt32(n); ;
 }
注意

注意事项为start,end,mid是索引而不是对应的数,由于猜数字游戏这个案例容易混淆索引和数值,需要注意。

总结

二分法的时间复杂度: 2可以省略

常见的几种大O运行时间。

下面从快到慢的顺序列出了你经常会遇到的5种大O运行时间

1.,也叫对数时间,这样的算法包括二分查找。

2.O(n), 也叫线性时间,这样的算法包括简单查找。

3.O(n*),这样的算法包括快速排序----------一种速度较快的排序算法

4.O,这样的算法包括选择排序-------------一种速度较慢的排序算法

5.O(n!),这样的算法包括旅行商问题的解决方案---------------一种非常慢的排序算法

Tips

旅行商问题(TravelingSalesmanProblem,TSP)是一个经典的组合优化问题。经典的TSP可以描述为:一个商品推销员要去若干个城市推销商品,该推销员从一个城市出发,需要经过所有城市后,回到出发地。应如何选择行进路线,以使总的行程最短。从图论的角度来看,该问题实质是在一个带权完全无向图中,找一个权值最小的Hamilton回路。由于该问题的可行解是所有顶点的全排列,随着顶点数的增加,会产生组合爆炸,它是一个NP完全问题。由于其在交通运输、电路板线路设计以及物流配送等领域内有着广泛的应用,国内外学者对其进行了大量的研究。早期的研究者使用精确算法求解该问题,常用的方法包括:分枝定界法、线性规划法、动态规划法等。但是,随着问题规模的增大,精确算法将变得无能为力,因此,在后来的研究中,国内外学者重点使用近似算法或启发式算法,主要有遗传算法模拟退火法蚁群算法禁忌搜索算法、贪婪算法神经网络等。

选择排序法

 //Tips
 /*
  * pop()方法是python的一个内置方法,可以通过列表、数组、字典、集合等的实例对象去调用。
  * 不加参数则是删除最后一个
  * arr.pop(2) 是删除索引2的元素并返回元素2的值
  * 也就是返回第三个元素的值 并删除它 形成一个新的数组
  */
 
 //选择排序法
 //eg:例如音乐app 你喜欢的歌曲歌单 里面的喜爱程度 是根据每首歌的播放次数来倒序排序的
 /***
  * 歌曲         播放次数  
  * a               10
  * b               9
  * c               8
  * d               7
  * e               6
  **/
 //每一次播放都会影响 需要找出最大的在第一个
 var list =new List<int>() {1,10,9,88,8,7,6,22 };
 var sortList = SelectSort(list);
 foreach (var item in sortList)
 {
    Console.Write(item+",");
 }
 Console.WriteLine();
 
 static List<int> SelectSort(List<int> list)
 {
    var listSort = new List<int>();
    //如果一开始不定下来 后面取动态的list.count会少(当循环的i次数等于list.count时候,后面的list直接丢失)
    //结果如下:88,22,10,9
    //正确 88,22,10,9,8,7,6,1,
    var maxleng = list.Count;
    for (int i = 0; i < maxleng; i++) {
        //找出这个list里面最大的元素
        var max = FindMax(list);
        //移除这个元素 然后继续去新的list去找最大的元素最终形成一个有序的从大到小的list
        listSort.Add(list[max]);
        list.RemoveAt(max);
 
    }
    //返回有序集合
    return listSort;
 }
 
 static int FindMax(List<int> list)
 {
    int max = list[0];
    int maxIndex = 0;
    for (int i = 0; i < list.Count; i++)
    {
        if (max < list[i])
        {
            max = list[i];
            maxIndex = i;
 
        }
    }
    return maxIndex;
 }
 
 Console.WriteLine("Hello, World!");
总结:

时间复杂度

选择排序是一种灵巧的算法,但其速度不是很快。快速排序是一种更快的排序算法,其运行时间为O(n*),这将在后面学习。

疑惑:

需要检查的元素越来越少?

随着排序的进行,每次需要检查的元素数在逐渐减少,最后一次需要检查的元素只有一个。既然如此,运行时间怎么还是O()呢?这个问题问得好,这与大O表示法中的常数相关。后面将在第四章详细学习,这里只是简单说一说。(本人手敲单纯只是想加深一遍印象而已)

你说的没错,并非每次都需要检查n个元素。第一次需要检查n个元素,但随后检查的元素数依次为n-1,n-2,

n-3,…… 2,1。平均每次检查的元素数为O,因此运行时间为O。但大O表示法省略如这样的常数,因此简单写作O(n x n) 或者 O()。

递归

 //Recursion 递归算法
 /*
  * 编写递归函数时,必须告诉它合适停止递归。正因为如此,每个递归函数都有两部分,基线条件(base case)和递归条件(recursion case)。
  * 递归条件指的是函数调用自己,而基线条件则指的是函数不再调用自己,从而避免无限循环。从下面Recursion方法中可以看到对应的基线条件和递归条件
  */
 
 //eg:套娃 比如一个大盲盒里面套盲盒 一直套 知道最后一个盲盒被你打开 这个过程 叫递归
 
 //简单的实现打印集合所有的元素
 var list = new List<int>() {1,2,3,4,5,6,7};
 Circle(list);
 
 Console.WriteLine("递归打印0~10的自然数");
 int n = 10;
 Recursion(n);
 
 
 static void Circle(List<int> list){
     //循环打印
     var count = list.Count;
     var i = 0;
     while (count > 0) {
         Console.WriteLine(list[i]);
         i++;
         count--;
    }
 }
 
 static void Recursion(int x)
 {
     if (x >= 0)
    {//递归条件就是x >= 0
         Console.WriteLine(x);
         Recursion(x - 1);
    }
     else { //基线条件
     //小于0 则跳出
     return;
    }  
 }
 
 Console.WriteLine("Hello, World!");
总结:(书中原文)

以上两种方法作用相同,但在我看来,第二种方法更清晰。递归只是让解决方案更清晰,并没有性能上的优势。实际上,在某些情况下,使用循环的性能更好。我很喜欢Leigh Caldwell在Stack Overflow上面说的一句话:”如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。如何选择要看什么对你来说更重要。“

很多算法都使用了递归,因此理解这种概念很重要。

 

Github 提交代码会有网络问题,后续代码可能会转向Gitee.

 
posted @ 2022-11-21 23:55  忽如一夜娇妹来  阅读(120)  评论(0编辑  收藏  举报