靈魂拷問:如何檢查 Java 數組中是否包含某個值?

2021-01-08 CSDN

作者 | 沉默王二

責編 | Elle

在逛 programcreek 的時候,我發現了一些專注細節但價值連城的主題。比如說:如何檢查Java數組中是否包含某個值 ?像這類靈魂拷問的主題,非常值得深入地研究一下。

另外,我想要告訴大家的是,作為程式設計師,我們千萬不要輕視這些基礎的知識點。因為基礎的知識點是各種上層技術共同的基礎,只有徹底地掌握了這些基礎知識點,才能更好地理解程序的運行原理,做出更優化的產品。

我曾在某個技術論壇上分享過一篇非常基礎的文章,結果遭到了無數的嘲諷:「這麼水的文章不值得分享。」我點開他的頭像進入他的主頁,發現他從來沒有分享過一篇文章,不過倒是在別人的博客下面留下過不少的足跡,大多數都是冷嘲熱諷。我就納悶了,技術人不都應該像我這樣低調謙遜嗎?怎麼戾氣這麼重!

好了,讓我們來步入正題。如何檢查數組(未排序)中是否包含某個值 ?這是一個非常有用並且經常使用的操作。我想大家的腦海中應該已經浮現出來了幾種解決方案,這些方案的時間複雜度可能大不相同。

我先來提供四種不同的方法,大家看看是否高效。

1)使用 List

publicstaticbooleanuseList(String[] arr, String targetValue){return Arrays.asList(arr).contains(targetValue);}

Arrays 類中有一個內部類 ArrayList(可以通過 Arrays.asList(arr) 創建該實例),其 contains() 方法的源碼如下所示。

public boolean contains(Object o) {return indexOf(o) != -1;}publicintindexOf(Object o) { E[] a = this.a;if (o == null) {for (int i = 0; i < a.length; i++)if (a[i] == null)return i; } else {for (int i = 0; i < a.length; i++)if (o.equals(a[i]))return i; }return-1;}

從上面的源碼可以看得出,contains() 方法調用了 indexOf() 方法,如果返回 -1 則表示 ArrayList 中不包含指定的元素,否則就包含。其中 indexOf() 方法用來獲取元素在 ArrayList 中的下標,如果元素為 null,則使用「==」操作符進行判斷,否則使用 equals() 方法進行判斷。

PS:關於「==」操作符和 equals() 方法,可以參照我另外一篇文章《如何比較 Java 的字符串?》

2)使用 Set

public static boolean useSet(String[] arr, String targetValue) {Set<String> set = new HashSet<String>(Arrays.asList(arr));returnset.contains(targetValue);}

HashSet 其實是通過 HashMap 實現的,當使用 new HashSet<String>(Arrays.asList(arr)) 創建並初始化了 HashSet 對象後,其實是在 HashMap 的鍵中放入了數組的值,只不過 HashMap 的值為默認的一個擺設對象。大家感興趣的話,可以查看一下 HashSet 的源碼。

我們來著重看一下 HashSet 的 contains() 方法的源碼。

publicbooleancontains(Object o){return map.containsKey(o);}publicbooleancontainsKey(Object key){return getNode(hash(key), key) != null;}

從上面的源碼可以看得出,contains() 方法調用了 HashMap 的 containsKey() 方法,如果指定的元素在 HashMap 的鍵中,則返回 true;否則返回 false。

3)使用一個簡單的循環

publicstatic boolean useLoop(String[] arr, String targetValue) {for (String s : arr) {if (s.equals(targetValue))returntrue; }returnfalse;}

for-each 循環中使用了 equals() 方法進行判斷——這段代碼讓我想起了幾個詞,分別是簡約、高效、清晰。

4)使用 Arrays.binarySearch()

publicstaticbooleanuseArraysBinarySearch(String[] arr, String targetValue){int a = Arrays.binarySearch(arr, targetValue);if (a > 0)returntrue;elsereturnfalse;}

不過,binarySearch() 只適合查找已經排序過的數組。

由於我們不確定數組是否已經排序過,所以我們先來比較一下前三種方法的時間複雜度。由於調用 1 次的時間太短,沒有統計意義,我們就模擬調用 100000 次,具體的測試代碼如下所示。

String[] arr = new String[]{"沉", "默", "王", "二", "真牛逼"};// 使用 Listlong startTime = System.nanoTime();for (int i = 0; i < 100000; i++) { useList(arr, "真牛逼");}long endTime = System.nanoTime();long duration = endTime - startTime;System.out.println("useList: " + duration / 1000000);// 使用 SetstartTime = System.nanoTime();for (int i = 0; i < 100000; i++) { useSet(arr, "真牛逼");}endTime = System.nanoTime();duration = endTime - startTime;System.out.println("useSet: " + duration / 1000000);// 使用一個簡單的循環startTime = System.nanoTime();for (int i = 0; i < 100000; i++) { useLoop(arr, "真牛逼");}endTime = System.nanoTime();duration = endTime - startTime;System.out.println("useLoop: " + duration / 1000000);

PS:nanoTime() 獲取的是納秒級,這樣計算的時間就更精確,最後除以 1000000 就是毫秒。換算單位是這樣的:1秒=1000毫秒,1毫秒=1000微秒,1微秒=1000納秒。

統計結果如下所示:

useList: 6useSet: 40useLoop: 2

假如把數組的長度增加到 1000,我們再來看一下統計結果。

String[] arr = newString[1000];Random s = new Random();for(int i=0; i< 1000; i++){ arr[i] = String.valueOf(s.nextInt());}

這時數組中是沒有我們要找的元素的。為了做比較,我們順便把二分查找也添加到統計項目中。

// 使用二分查找startTime = System.nanoTime();for (int i = 0; i < 100000; i++) { useArraysBinarySearch(arr, "真牛逼");}endTime = System.nanoTime();duration = endTime - startTime;System.out.println("useArraysBinarySearch: " + duration / 1000000);統計結果如下所示:useList: 91useSet: 1460useLoop: 70useArraysBinarySearch: 4

我們再把數組的長度調整到 10000。

String[] arr = newString[10000];Random s = new Random();for(int i=0; i< 10000; i++){ arr[i] = String.valueOf(s.nextInt());}

統計結果如下所示:

useList: 1137useSet: 15711useLoop: 1115useArraysBinarySearch: 5

從上述的統計結果中可以很明顯地得出這樣一個結論:使用簡單的 for 循環,效率要比使用 List 和 Set 高。這是因為把元素從數組中讀出來再添加到集合中,就要花費一定的時間,而簡單的 for 循環則省去了這部分時間。

在得出這個結論之前,說實話,我最喜歡的方式其實是第一種「使用 List」,因為只需要一行代碼 Arrays.asList(arr).contains(targetValue) 就可以搞定。

雖然二分查找(Arrays.binarySearch())花費的時間明顯要少得多,但這個結論是不可信的。因為二分查找明確要求數組是排序過的,否則查找出的結果是沒有意義的。可以看一下官方的 Javadoc。

Searches the specified array for the specified object using the binary search algorithm. The array must be sorted into ascending order according to the natural ordering of its elements (as by the sort(Object []) method) prior to making this call. If it is not sorted, the results are undefined.

實際上,如果要在一個數組或者集合中有效地確定某個值是否存在,一個排序過的 List 的算法複雜度為 O(logn),而 HashSet 則為 O(1)。

我們再來發散一下思維:怎麼理解 O(logn) 和 O(1) 呢?

O(logn) 的算法複雜度,比較典型的例子是二分查找。舉個例子,假設現在一堆試卷,已經按照分數從高到底排列好了。現在要查找有沒有 79 分的試卷,怎麼辦呢?可以先從中間找起,因為按照 100 分的卷子來看,79 分大差不差應該就在中間的位置(平均分如果低於 79 說明好學生就比較少了),如果中間這份卷子的分數是 83,那說明 79 分的卷子就在下面的一半,這時候可以把上面那半放在一邊了。然後按照相同的方式,每次就從中間開始找,直到找到 79 分的卷子(當然也可能沒有 79 分)。

假如有 56 份卷子,找一次,還剩 28 份,再找一次,還剩 14 份,再找一次,還剩 7 份,再找一次,還剩 2 或者 3 份。如果是 2 份,再找一次,就只剩下 1 份了;如果是 3 份,就還需要再找 2 次。

我們知道,log2(32) = 5,log2(64) = 6,而 56 就介於 32 和 64 之間。也就是說,二分查找大約需要 log2(n) 次才能「找到」或者「沒找到」。而在算法複雜度裡,經常忽略常數,所以不管是以 2 為底數,還是 3 為底數,統一寫成 log(n) 的形式。

再來說說 O(1),比較典型的例子就是哈希表(HashSet 是由 HashMap 實現的)。哈希表是通過哈希函數來映射的,所以拿到一個關鍵字,通過哈希函數轉換一下,就可以直接從表中取出對應的值——一次直達。

好了各位讀者朋友們,以上就是本文的全部內容了。能看到這裡的都是最優秀的程式設計師,我必須要為大家點個讚。

聲明:本文為作者投稿,版權歸作者個人所有。

【End】

相關焦點

  • PHP函數in_array()如何檢查數組中的值
    PHP函數in_array()如何檢查數組中的值 PHP函數in_array()可以幫助我們輕鬆的完成對數組中某個值的檢查。我們下面就舉一個例子來幫助大家理解PHP函數in_array()的具體應用。
  • 一起學JAVA——數組和函數
    如果某個函數在執行的時候需要調用者傳入數據,那麼可以定義參數列表,用於接收數據。如果函數運行之後需要返回給調用者數據,那麼需要指定返回值類型,並且用關鍵字return返回。定義函數的3個必要條件:函數名、參數列表、返回值類型。
  • 在VBA中如何使用動態數組,以及利用動態數組去除重複值的方法
    大家好,我們今日繼續講解VBA數組與字典解決方案第22講:在VBA中如何使用動態數組,以及利用動態數組去除重複值的方法。如果文本中含有大量的重複值,此時,如果我們要剔除重複值,該怎麼辦?用VBA的方法該如何做到呢?我在這講和下一講中將解答這個問題,並提供給讀者一個可以測試的實例。今日先講這個內容要用到的知識點。
  • java之數組作為方法參數的使用
    各位小夥伴們大家好,這次小編要介紹的是java作為方法參數,返回值的使用。首先,小編要介紹的是java作為方法參數的使用。1.返回值類型:只是進行列印,不需要進行計算,也沒有結果,用void* 2.方法名稱:printArray* 3.參數列表:必須有數組,才可以列印其中的元素。
  • LeetCode數組類知識點&題型總結
    題型總結這類題目通常會給定一個數組和一個值,讓求出這個數組中兩個/三個/K個值的和等於這個給定的值target。解法如下:Leetcode中包含該類型的題目:序號題目難度代碼1Two Sumeasypython、java、c++167Two Sum II-Input array is sortedeasypython、java、c++153Summediumpython、java、c++163Sum Closetmediumpython、java、c++2593Sum
  • 2020年Java基礎高頻面試題匯總
    int默認值是0,而Integer默認值是null,所以Integer能區分出0和null的情況。一旦java看到null,就知道這個引用還沒有指向某個對象,2.基本數據類型在聲明時系統會自動給它分配空間,而引用類型聲明時只是分配了引用空間,必須通過實例化開闢數據空間之後才可以賦值。
  • Java8 lambda表達式
    匿名內部類是為了讓java程式設計師傳遞行為和傳遞數據一樣容易,不幸的是,他們並不容易,為了調用處理邏輯的代碼仍然有四行模板代碼,重複的模板代碼並不是唯一的問題,這種代碼也難以閱讀,我們並不想傳遞一個對象,而僅僅只需要傳遞某種行為,在java8中我們可以寫得更簡潔不同於傳遞一個實現某個接口的對象,我們傳遞了一段沒有命名函數的代碼
  • 「JAVA」萬字長篇詳述字節碼對象與反射機制完成動態編程
    連結包含三個步驟:驗證:檢測被加載的類是否有正確的內部結構。準備:負責為類的static變量分配內存,並根據變量的數據類型設置默認值。解析:把字節碼指令中對常量池中的索引引用轉換為直接引用。Java 中,所有的數據類型都有class屬性,包括基本數據類型;如何獲取呢?使用字面量的方式,這種方式不僅可以應用於類,還可以應用於接口、數組、和基本數據類型。
  • java數組刪除重複元素專題及常見問題 - CSDN
    package com.akfucc.zhidao;import java.util.ArrayList;import java.util.Collections;import java.util.Iterator;import java.util.List
  • Java編程中基礎反射詳細解析
    類的連接又分為下面三個階段:驗證:確保被加載類的正確性準備:負責為類的靜態成員分配內存,並設置默認初始化值解析:將類中的符號引用替換為直接引用1.3 類的初始化類加載的時機創建類的實例的時候訪問類的靜態變量的時候調用類的靜態方法的時候使用反射方式來強制創建某個類或接口對應的java.lang.Class對象初始化某個類的子類的時候
  • 如何使用Numpy數組?
    【連續「Python利用Numpy數組進行數據處理(一)」】2.【聚合函數】數學和統計方法[軸和0]可以通過數組上的一組數學函數對整個數組或某個軸向的數組進行統計計算。>說明sum對數組中全部或某軸向的元素求和。
  • Java反射初探 ——「當類也學會照鏡子」
    每個構造器都對應有一個保存和該構造器有關信息的Constructor對象,這個對象所屬的類是java.lang.reflect.Constructor 方法,成員變量和構造器是附屬於某一個類的,正因如此,我們應該先取得某一個類對應的Class對象,其次才考慮如何取得 Method/Field/Constructor對象
  • Java反射機制深入詳解
    反射是java語言的一個特性,它允程序在運行時(注意不是編譯的時候)來進行自我檢查並且對內部的成員進行操作。例如它允許一個java的類獲取他所有的成員變量和方法並且顯示出來。Java 的這一能力在實際應用中也許用得不是很多,但是在其它的程序設計語言中根本就不存在這一特性。例如,Pascal、C 或者 C++ 中就沒有辦法在程序中獲得函數定義相關的信息。
  • 如何獲取numpy數組的真實地址?如何與ctypes數組共享內存?
    在Python編程中,numpy是一個很好用的擴展程序庫,將其與SciPy庫和 Matplotlib繪圖庫一起使用,可構成一個強大的類似於Matlab的科學計算環境,有助於我們通過 Python 學習數據科學或者機器學習。在Python中,當你定義了一個numpy類型的數組後,它內部元素的真實地址如何獲得呢?
  • 數據結構java面試題及答案
    2、如何找到一個給定的整型數組中的重複數字?3、在一個未排序的整型數組中,如何找到最大和最小的數字?4、在一個整型數組中,如何找到一個所有成對的數字,滿足它們的和等於一個給定的數字?5、如果一個數組包含多個重複元素,如何找到這些重複的數字?6、用Java實現從一個給定數組中刪除重複元素?
  • Python編程:如何規範numpy中數組元素的列印輸出格式
    在代碼調試過程中,我們經常會使用print函數列印查看numpy數組元素的運算結果,那麼如何規範或者自定義這種數組的輸出格式呢?函數set_printoptions原型numpy庫中提供了一個函數set_printoptions,通過這個函數可對列印結果進行各種設置。
  • 反射——Java高級開發必須懂得
    描述:在 main 函數中,有一個 String args[] 參數,這就表示在執行某 .class 文件時,可以對 main 函數傳字符串參數(例如:命令行中:java OfficeBetter Excel,傳給主函數的參數就是Excel,如果傳多個參數,參數用空格隔開),Office類中使用了兩個類(沒有提供這兩個類),並調用其相應的方法
  • 提升java編程性能優化知識 程式設計師必看這幾點
    對於學習java的學子也是如此,那麼java程式設計師如何提高編程性能呢,有哪些小知識或者技巧呢,怎麼樣才能在編程性能優化方面有所提升呢?  1.儘量在合適的場合使用單例  使用單例可以減輕加載的負擔,縮短加載的時間,提高加載的效率,但並不是所有地方都適用於單例,簡單來說,單例主要適用於以下三個方面:
  • Java基礎篇——數組詳解
    使用數組可以將同一類型的數據存儲在連續的內存位置。數組中各元素的類型相同,通過下標的方式來訪問數組中的元素,下標從0開始。由此得出,數組具有以下基本特點:數組的長度是確定的,數組一旦被創建,它的大小就是不可以改變的。
  • java基礎教程:Collection集合,Collection 常用API
    集合:集合是java中提供的一種容器,可以用來存儲多個數據。集合和數組既然都是容器,它們有什麼區別呢?數組的長度是固定的。集合的長度是可變的。數組中存儲的是同一類型的元素,可以存儲任意類型數據。集合存儲的都是引用數據類型。如果想存儲基本類型數據需要存儲對應的包裝類型。