算法:時(shí)間復(fù)雜度+二分查找法(Java/Go/Python)實(shí)現(xiàn)

導(dǎo)讀

曾幾何時(shí)學(xué)好數(shù)據(jù)結(jié)構(gòu)與算法是我們從事計(jì)算機(jī)相關(guān)工作的基本前提,然而現(xiàn)在很多程序員從事的工作都是在用高級(jí)程序設(shè)計(jì)語言(如Java)開發(fā)業(yè)務(wù)代碼,久而久之,對于數(shù)據(jù)結(jié)構(gòu)和算法就變得有些陌生了,由于長年累月的碼磚的緣故,導(dǎo)致我們都快沒有這方面的意識(shí)了,雖然這種論斷對于一些平時(shí)特別注重學(xué)習(xí)和思考的人來說不太適用,但的確是有這樣的一個(gè)現(xiàn)象。

而在要出去面試找工作的時(shí)候,才發(fā)現(xiàn)這些基礎(chǔ)都快忘光光了,所以可能就“杯具”了!實(shí)際上,對于數(shù)據(jù)結(jié)構(gòu)和算法相關(guān)的知識(shí)點(diǎn)的學(xué)習(xí),是程序員必須修煉的一門內(nèi)功,而要掌握得比較牢靠除了需要在寫代碼的時(shí)候時(shí)刻保持這方面的意識(shí)外,也需要日常的訓(xùn)練,換一個(gè)目前比較流行的詞叫刻意練習(xí)

這就像打乒乓球一樣,雖然大家都會(huì)打,但是要打得好,打出水準(zhǔn)就得經(jīng)常訓(xùn)練。而學(xué)習(xí)算法的過程也是這樣,因?yàn)榇蟛糠秩说哪X容量有限,對于學(xué)過的算法知識(shí)雖然之前理解過,但是因?yàn)闀r(shí)間的關(guān)系和算法本身就是比較抽象的一種知識(shí),所以容易忘記。那么有沒有什么好的練習(xí)工具呢?

在這里給大家推薦一個(gè)練習(xí)數(shù)據(jù)結(jié)構(gòu)和算法編程的網(wǎng)站

https://leetcode.com(因?yàn)閴Φ脑?,你可能需要搭個(gè)梯子,或者也可以訪問中文版的網(wǎng)站)這是一個(gè)目前很多硅谷的公司或程序員在學(xué)習(xí)或者招聘時(shí)都在使用的在線練習(xí)網(wǎng)站。上面有很多數(shù)據(jù)結(jié)構(gòu)和算法的題,可以選擇不同的編程語言實(shí)現(xiàn),還支持代碼社交,你提交的代碼可以被全世界的程序員看到并被評論,從而得到相應(yīng)地反饋。

以本文將要講述的二分查找算法為例,在給大家的代碼示例中作者就在這個(gè)網(wǎng)站上使用Java/Go/Python三種語言進(jìn)行了實(shí)現(xiàn),如??圖所示:

算法復(fù)雜度

因?yàn)槲沂窍胱鲆粋€(gè)比較系統(tǒng)的總結(jié),所以在給大家分享具體的算法內(nèi)容之前,需要先和大家一起回顧下什么是算法復(fù)雜度

在編程的過程中,特別是寫一些比較基礎(chǔ)的邏輯代碼時(shí),我們經(jīng)常會(huì)討論說這段代碼的時(shí)間復(fù)雜度是多少,空間復(fù)雜度是多少之類的術(shù)語。而這兩項(xiàng)指標(biāo)就是我們衡量我們寫的代碼(任何代碼片段都可以視為算法)好壞最主要的兩個(gè)標(biāo)準(zhǔn)了,時(shí)間復(fù)雜度是說我們寫的這段代碼的運(yùn)行時(shí)間,而空間復(fù)雜度則是在說這段代碼運(yùn)行所占用的內(nèi)存空間大小。

一般來說,我們在選擇算法、編寫相應(yīng)的代碼時(shí)都應(yīng)該盡量選擇運(yùn)行時(shí)間最快,內(nèi)存空間占用最少的方式。然而作為衡量算法好壞的標(biāo)準(zhǔn),關(guān)于時(shí)間復(fù)雜度、空間復(fù)雜度如何衡量?目前是通過大O表示法來表示的,也就是我們經(jīng)常說的O(xx),例如O(1)、O(n)之類。

以下是我們常見的一些大O運(yùn)行時(shí)間的表示(從快到慢):

這是一種常數(shù)級(jí)的復(fù)雜度,這種復(fù)雜度的算法運(yùn)行效率是最高的。例如,我們要計(jì)算“1+2+3+...+n”的和(假設(shè)n=100)?

如果我們采用如??方式實(shí)現(xiàn),那么100的累加和的計(jì)算,這段代碼需要執(zhí)行100次,1000累加和則需要執(zhí)行1000次。

 public static void main(String args[]) {

       int y = 0;
       for (int i = 0; i <= 100; i++) {
           y = i + y;
       }
       System.out.println(y);
   }

而如果我們換種方式如??


  public static void main(String args[]) {

        int y = 0;
        y = 100 * (100 + 1) / 2;
        System.out.println(y);
    }

那么無論多少的累加和,10000、100000、100000?上面這段程序都是需要執(zhí)行一次,此時(shí)我們就可以說這段代碼的時(shí)間復(fù)雜度是O(1)了。

這是一種對數(shù)級(jí)的復(fù)雜度??赡苌蠈W(xué)的時(shí)候是因?yàn)轶w育老師教數(shù)學(xué)的緣故小碼農(nóng)已經(jīng)忘記什么是對數(shù)了,在這里和大家一起復(fù)習(xí)下,假如你忘記了對數(shù)的概念,但是冪的概念你一定還是記得的log8相當(dāng)于在問“將多少個(gè)2相乘的結(jié)果為8”,正常來說log對數(shù)還有個(gè)下標(biāo),因?yàn)槲覀冊谟懻撍惴◤?fù)雜度時(shí)通常默認(rèn)對數(shù)的下標(biāo)為2,如2x2x2=8,所以log8=3。

那么什么樣的算法的時(shí)間復(fù)雜度是對數(shù)級(jí)的呢?后面我們即將討論的第一種算法(二分查找法)的時(shí)間復(fù)雜度就是對數(shù)級(jí)的,關(guān)于這塊的代碼示例,大家可以直接參考后面的示例。

O(n)是一種線性的時(shí)間復(fù)雜度,如前面我們在說0(1)時(shí),如果計(jì)算累加和的操作采用第一種方式,那么100的累加和需要執(zhí)行100次,1000的累加和就需要執(zhí)行1000次,以此類推,這樣的時(shí)間復(fù)雜度就稱之為O(n)。

O(nlogn)是O(logn)、O(n)兩種復(fù)雜度的一種組合,在后續(xù)的文章中要給大家介紹的“快速排序算法”(一種速度較快的排序算法),其時(shí)間復(fù)雜度就是O(nlogn),這里大家可以暫時(shí)先放一下,等后面具體講述此算法時(shí),可以體會(huì)下相應(yīng)的代碼示例。

平方級(jí)的復(fù)雜度,在后續(xù)要介紹的算法中,“選擇排序算法”(一種速度較慢的算法)的時(shí)間復(fù)雜度就是這個(gè)級(jí)別的。如??

public static void main(String args[]) {

       int[] arr = new int[]{4, 1, 3, 2, 6, 7, 8};
       for (int i = 0; i < arr.length; i++) {
           int m = i;
           for (int j = i + 1; j < arr.length; j++) {
               //如果第j個(gè)元素比第m個(gè)元素小,將j賦值給m
               if (arr[j] < arr[m]) {
                   m = j;
               }
           }
           //交換m和i兩個(gè)元素的位置
           if (i != m) {
               int t = arr[i];
               arr[i] = arr[m];
               arr[m] = t;
           }
       }

       for (int i = 0; i < arr.length; i++) {
           System.out.print(arr[i]);
           System.out.print(",");

       }

   }

階乘級(jí)的復(fù)雜度表示,時(shí)間復(fù)雜度為O(n!)的算法是一個(gè)非常慢的算法,例如斐波那契數(shù)列問題。如??

 public static int fib(int num) {
        if (num == 1 || num == 2) {
            return 1;
        } else {
            return fib(num - 2) + fib(num - 1);
        }
    }

    public static void main(String[] args) {

        for (int i = 1; i <= 6; i++) {
            System.out.print(fib(i) + "\t");
        }
    }

以上就是我們在討論算法復(fù)雜度時(shí)大部分的表示方法了,需要說明的是算法的速度并非單單指時(shí)間,而是操作數(shù)的增速,說的是隨著輸入的增加,其運(yùn)行時(shí)間將以什么樣的速度增加,例如O(log n)比O(n)快,當(dāng)需要搜索的元素越多時(shí),O(log n)比O(n)快得越多。

二分查找法

在了解算法復(fù)雜度后,我們來介紹一個(gè)相對基礎(chǔ)的算法“二分查找法”!其輸入是一個(gè)有序的元素列表(必須有序),如果查找的元素包含在這個(gè)有序列表中,二分查找就會(huì)返回其位置,否則返回-1。

假設(shè)有一個(gè)1~100的數(shù)字:

目標(biāo)是以最少的次數(shù)從這個(gè)100個(gè)數(shù)字中找到指定的數(shù)字。通常思路是,我們需要從1開始一個(gè)一個(gè)比較,如果指定的數(shù)字是100,那么用這種傻找的方式需要找100次。如果范圍擴(kuò)大到1000,查找1000,那么相應(yīng)地就需要找1000次,依次類推。

很顯然,這種方式不是很靠譜。那么有沒有更好的方法呢?這就是我們要說的二分查找法了,它的思路是先從元素的中間開始查找,如直接查找第50(第一次)個(gè)元素,比較中間元素與目標(biāo)元素之間的大小。

如果我們還是查找100的話,那么50比100小,這樣我們就可以一次性排除前50個(gè)元素了,因?yàn)橐呀?jīng)很明確的知道了前50個(gè)元素都比目標(biāo)元素小了,這些元素也就沒有必要繼續(xù)參與比較了。

第二次,我們從51~100之間再取中間元素進(jìn)行判斷,取75進(jìn)行判斷,75依然比100小,那么51~75這一段的數(shù)字也就直接排除掉了。

第三次,我們從76~100之間取中間元素,88,88<100;第四次,繼續(xù)查找取89~100之間的中間元素95,95<100;第五次,繼續(xù)取96~100中間元素98,98<100;第六次,繼續(xù)取99~100之間的中間元素100,100=100,完成查找。

通過這種方式,我們總共運(yùn)行了6次就完成了查找動(dòng)作,相比之前的100次查找要靠譜,而這就是二分查找算法的基本原理了。

按照這種方式,即使列表中包含40億個(gè)有序元素,最多也只需要32次就能完成查找。所以如果用前面描述的大O表示法,表示二分查找算法的時(shí)間復(fù)雜度是O(log n)。

好了,到這里就講完二分查找算法的基本原理了,那么在具體的程序代碼中,二分查找算法應(yīng)該如何實(shí)現(xiàn)呢?以下為大家準(zhǔn)備了Java/Go/Python三種版本的實(shí)現(xiàn),大家可以在leetcode上嘗試自己實(shí)現(xiàn)下。

#Java

public class Solution {

    public static int search(int[] nums, int target) {
        int low = 0;
        int high = nums.length - 1;
        int middle;
        while (low <= high) {
            middle = (low + high) / 2;
            if (nums[middle] == target) {
                return middle;
            } else if (nums[middle] < target) {
                low = middle + 1;
            } else{
                high = middle - 1;
            } 
        }
        return -1;
    }

    public static void main(String[] args) {
        int[] nums = {-1, 0, 3, 5, 9, 12};
        int result1 = search(nums, 9);
        int result2 = search(nums, 2);
        System.out.println("result is->" + result1);
        System.out.println("result is->" + result2);
    }
}

#Go

package main

import (
    "fmt"
)

func main() {
    nums := []int{-1, 0, 3, 5, 9, 12}
    result := search(nums, 2)
    fmt.Println(result)
}

func search(nums []int, target int) int {
    low, high, middle := 0, len(nums)-1, -1
    for low <= high {
        middle = (low + high) / 2
        if nums[middle] == target {
            return middle
        } else if nums[middle] < target {
            low = middle + 1
        } else {
            high = middle - 1
        }
    }
    return -1
}

#Python

class Solution:
    def search(result,nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """
        low,high,middle=0,len(nums)-1,-1
        while low<=high:
           middle=(low+high)//2
           if nums[middle]==target:
               return middle
           elif nums[middle]<target:
               low=middle+1
           else:
               high=middle-1
        return -1
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容