算法入门相关1

首先来了解一下什么是大O表示法:
为什么要叫大O表示法?其实很简单,只是因为它的前面是个大O而已,没有特殊含义。

下面介绍一下大O表示法的定义:

假设我们需要在一排有序的数字中找到一个特定的数字。例如:1,2,4,7,9,13,23,33.我们需要在其中找到33。

有两种解决方式,一种是简单查找,一种是二分查找。

使用简单查找的话,我们需要将所有的元素从第一个开始向后遍历,在最坏的情况下,我们需要进行8次操作,因为其中有八个操作数,而简单查找的最坏情况总是由操作数个数来决定的,所以它的时间复杂度为O(n)。

而使用二分查找法

因为二分查找法每一次的操作都会使查找范围减半,所以对于上述需求,二分查找法在最差的情况下也只要执行三次,所以它的时间复杂度为O(logn)。
在这里我们需要注意的是,大O表示法的目的在于表示算法的增速,即在操作数逐渐增加的情况下,算法执行所需要的时间的增长速度。而对于二分查找法的时间复杂度O(logn)来说这里的底数永远是2,这是规定好的。
Java中我有印象是自带了二分查找的方法的,并且二分查找法只能用在有序的数据体中。

二分查找法实现如下:

public static void Binary(int[] arr,int guess){
     int low = 0;
     int high = arr.length-1;
     while(low<=high){
         int mid = (low+high)/2;
         if(arr[mid] == guess){
             System.out.println("该数组中存在该元素");
             return;
         }else if(arr[mid]<guess){
             low = mid;
         }else {
             high = mid;
         }
         System.out.println("该数组中不存在该元素");
         return;
         }
     }

总结如下:
算法的速度指的并非时间,而是操作数的增速。
谈论算法的速度时,我们说的是随着输入的增加,其运行时间将以什么样的速度增加。
算法的运行时间用大O表示法表示。
O(log n)比O(n)快,当需要搜索的元素越多时,前者比后者快得越多。

接下来我们来了解选择排序,排序算法在很多语言中都自带,但是我们应该理解其中的原理。在此之前需要有数据结构中数组和链表的知识,不知道可以查看前面的关于数据结构的博客。

假设我们有一个歌曲列表,我们需要通过它们的播放量来对它们进行排序,思路是先找出列表中播放量最少的排在第一位,然后根据同样的方法排在第二第三位等。

具体实现如下:

public static void SelectionSort(int arr[]){
        for(int i = 0; i<arr.length-1; i++){
            int k = i;
            for(int j = k+1; j<arr.length; j++){
                if(arr[k]>arr[j]){
                    k=j;
                }
            }
            if(i != k){
                int temp = arr[i];
                arr[i] = arr[k];
                arr[k] = temp;
            }
        }
    }

在选择排序中,第一次需要检查n个元素,但随后检查的元素数依次为n-1, n - 2, …, 2和1。平均每次检查的元素数为1/2 × n,因此运行时间为O(n × 1/2 × n)。但大O表示法省略诸如1/2这样的常数,因此简单地写作O(n × n)或O(n^2)。可以看到它的速度并不是很快,有一种更快的排序算法,就是快速排序,后续会说到它。

循环和递归

假设我们现在要在一堆盒子中找到一把钥匙,盒子里有可能是盒子也有可能是钥匙,为找到钥匙,我们需要用什么算法?
第一种办法:
1.创建一个盒子堆
2.从盒子堆中取出一个盒子,打开盒子
3.如果是盒子,就放回盒子堆中
4.如果是钥匙,就完成了事件
5.回到第二步

伪代码如下:

def look_for_key(main_box):
 pile = main_box.make_a_pile_to_look_through()
 while pile is not empty:
 box = pile.grab_a_box()
 for item in box:
if item.is_a_box():
pile.append(item)
elif item.is_a_key():
print "found the key!" 

第二种方法:
1.检查盒子中的东西
2.如果是盒子就回到第一步
3.如果是钥匙则完成

def look_for_key(box):
 for item in box:
 if item.is_a_box():
look_for_key(item)
 elif item.is_a_key():
print "found the key!"

第一种方法就是循环,第二种则是递归

在很多功能的实现步骤中,都需要不断得对不同的操作数进行同样的操作,我们可以使用循环或者递归来完成。两者各有各的好处,在很多情况下,使用循环的性能会更好,但递归则更容易让人理解,在很多算法中都使用到了递归,所以了解递归是必须的。

编写递归函数时,必须告诉它何时停止递归。正因为如此,每个递归函数都有两部分:基线条件和递归条件。递归条件指的是函数调用自己,而基线条件则指的是函数不再调用自己,从而避免形成无限循环。

计算机在内部使用名为调用栈的栈,栈是一种数据结构,可以参考以前的数据结构博客。当一个函数被调用时,计算机会首先为这个函数分配一块内存,其被压入调用栈底,内存中存入需要存储的变量。然后当这个函数在运行中调用其他函数时,被调用函数分配的内存就会压在原函数上,只有被调用函数运行完,原函数才能结束。递归函数也使用这种调用栈。

回到原来的问题,当使用第一种办法取钥匙时,因为我们定义了一个盒子堆,所以始终知道还有哪些盒子没有找完,但使用递归时,没有盒子堆,要算法怎么知道还有哪些盒子没有找完呢?

这就使用到了调用栈,调用栈中保留着未完成的函数调用,而函数调用中包含着没有找完的盒子,这就是栈的好处,你无需跟踪盒子堆,栈会自动帮你追踪。

递归是非常方便的,但使用它也要付出代价,每个函数调用都需要占据一部分内存,如果调用栈非常高,就代表计算机中存储了大量函数调用的信息,在这种情况下,只有两种办法:
1.重新编写代码转成循环

2.使用尾递归

尾递归的定义是所有递归形式的调用都出现在末尾,之后没有任何内容,那么它就是尾递归。
**普通递归的执行方式是外层函数依赖内层函数返回的结果,然后由最外层结束操作。而尾递归则是将外层函数的结果传递给内层函数,那么在这个运行中外层函数就没有任何意义了,所以可以直接在栈中被覆盖掉。**当编译器加检测到一个递归函数是尾递归时,它并不会给下一层递归赋予新的空间,而是将外层函数使用的空间直接覆盖,这样就可以避免普通递归调用栈过高的问题。

  • 3
    点赞
  • 1
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值