web-dev-qa-db-fra.com

Swift: recherche binaire pour un tableau standard?

J'ai un tableau trié et je veux faire une recherche binaire dessus.

Je demande donc si quelque chose est déjà disponible dans la bibliothèque Swift, comme par exemple, etc.? Ou existe-t-il une version indépendante du type?

Bien sûr, je pourrais l'écrire moi-même, mais j'aime bien éviter de réinventer la roue.

18
Peter71

Voici une manière générique d'utiliser la recherche binaire:

func binarySearch<T:Comparable>(inputArr:Array<T>, searchItem: T) -> Int? {
    var lowerIndex = 0;
    var upperIndex = inputArr.count - 1

    while (true) {
        let currentIndex = (lowerIndex + upperIndex)/2
        if(inputArr[currentIndex] == searchItem) {
            return currentIndex
        } else if (lowerIndex > upperIndex) {
            return nil
        } else {
            if (inputArr[currentIndex] > searchItem) {
                upperIndex = currentIndex - 1
            } else {
                lowerIndex = currentIndex + 1
            }
        }
    }
}

var myArray = [1,2,3,4,5,6,7,9,10];
if let searchIndex = binarySearch(myArray,5){
    println("Element found on index: \(searchIndex)");
}
13
Daniel Krom

Voici mon implémentation préférée de la recherche binaire. C'est utile non seulement pour trouver l'élément mais aussi pour trouver l'index d'insertion. Les détails concernant l'ordre de tri supposé (croissant ou décroissant) et le comportement vis-à-vis des éléments égaux sont contrôlés en fournissant un prédicat correspondant (par exemple { $0 < x } vs { $0 > x } vs { $0 <= x } vs { $0 >= x }). Le commentaire dit sans ambiguïté ce qu'il fait exactement.

extension RandomAccessCollection {
    /// Finds such index N that predicate is true for all elements up to
    /// but not including the index N, and is false for all elements
    /// starting with index N.
    /// Behavior is undefined if there is no such N.
    func binarySearch(predicate: (Element) -> Bool) -> Index {
        var low = startIndex
        var high = endIndex
        while low != high {
            let mid = index(low, offsetBy: distance(from: low, to: high)/2)
            if predicate(self[mid]) {
                low = index(after: mid)
            } else {
                high = mid
            }
        }
        return low
    }
}

Exemple d'utilisation:

(0 ..< 778).binarySearch { $0 < 145 } // 145
34
Vadim Yelagin

J'utilise un extension sur Indexable implémentant indexOfFirstObjectPassingTest.

  • Il faut un prédicat test et renvoie l'index du premier élément pour réussir le test. 
  • S'il n'y a pas d'index, alors il retourne endIndex de la Indexable
  • Si la Indexable est vide, vous obtenez la endIndex.

Exemple

let a = [1,2,3,4]

a.map{$0>=3}
// returns [false, false, true, true]

a.indexOfFirstObjectPassingTest {$0>=3}
// returns 2

Important

Vous devez vous assurer que test ne retourne jamais dans false pour tout index après un index pour lequel il a déclaré true. Cela équivaut à la condition préalable habituelle selon laquelle la recherche binaire exige que vos données soient en ordre.

Plus précisément, vous ne devez pas le fairea.indexOfFirstObjectPassingTest {$0==3}. Cela ne fonctionnera pas correctement.

Pourquoi?

indexOfFirstObjectPassingTest est utile car il vous permet de rechercher des plages des éléments dans vos données. En ajustant le test, vous pouvez trouver les limites inférieure et supérieure de "substance".

Voici quelques données:

let a = [1,1,1, 2,2,2,2, 3, 4, 5]

Nous pouvons trouver la Range de tous les 2s comme ceci…

let firstOf2s = a.indexOfFirstObjectPassingTest({$0>=2})
let endOf2s = a.indexOfFirstObjectPassingTest({$0>2})
let rangeOf2s = firstOf2s..<endOf2s
  • S'il n'y a pas de 2s dans les données, on récupérera une plage vide} _ et nous n'aurons besoin d'aucun traitement particulier. 
  • À condition qu'il y ait 2s, nous les trouverons tous.

Par exemple, je l’utilise dans une implémentation de layoutAttributesForElementsInRect. Ma UICollectionViewCells sont stockés triés verticalement dans un tableau. Il est facile d'écrire une paire d'appels qui trouveront toutes les cellules qui se trouvent dans un rectangle particulier et en excluent les autres.

Code

extension Indexable {
  func indexOfFirstObjectPassingTest( test: (Self._Element -> Bool) ) -> Self.Index {
    var searchRange = startIndex..<endIndex

    while searchRange.count > 0 {
      let testIndex: Index = searchRange.startIndex.advancedBy((searchRange.count-1) / 2)
      let passesTest: Bool = test(self[testIndex])

      if(searchRange.count == 1) {
        return passesTest ? searchRange.startIndex : endIndex
      }

      if(passesTest) {
        searchRange.endIndex = testIndex.advancedBy(1)
      }
      else {
        searchRange.startIndex = testIndex.advancedBy(1)
      }
    }

    return endIndex
  }
}

Avertissement et mise en garde

J'ai environ 6 ans d'expérience iOS, 10 ans en Objective C et plus de 18 ans en programmation… 

… Mais je suis au jour 3 de Swift :-)

  1. J'ai utilisé une extension sur le protocole Indexable. Cette approche pourrait être stupide - les commentaires sont les bienvenus.
  2. Les recherches binaires sont notoirement difficiles à coder correctement. Vous devriez vraiment lire ce lien pour découvrir à quel point les erreurs dans leur implémentation sont courantes, mais voici un extrait:

Lorsque Jon Bentley a attribué ce problème à un cours destiné aux programmeurs professionnels, il a constaté que 90% des personnes interrogées n'avaient pas réussi à coder correctement une recherche binaire après plusieurs heures de travail, et une autre étude a révélé que code précis trouvé que dans cinq manuels sur vingt. En outre, la propre mise en œuvre de la recherche binaire par Bentley, publiée dans son ouvrage de 1986, Programming Pearls, contient une erreur qui n'a pas été détectée pendant plus de vingt ans.

Compte tenu de ce dernier point, voici un test pour ce code. Ils passent. Ils sont peu susceptibles d’être exhaustifs - il peut donc encore y avoir des erreurs. Les tests ne sont pas forcément corrects! Il n'y a pas de tests pour les tests.

Des tests

class BinarySearchTest: XCTestCase {

  func testCantFind() {
    XCTAssertEqual([].indexOfFirstObjectPassingTest {(_: Int) -> Bool in false}, 0)
    XCTAssertEqual([1].indexOfFirstObjectPassingTest {(_: Int) -> Bool in false}, 1)
    XCTAssertEqual([1,2].indexOfFirstObjectPassingTest {(_: Int) -> Bool in false}, 2)
    XCTAssertEqual([1,2,3].indexOfFirstObjectPassingTest {(_: Int) -> Bool in false}, 3)
    XCTAssertEqual([1,2,3,4].indexOfFirstObjectPassingTest {(_: Int) -> Bool in false}, 4)
  }

  func testAlwaysFirst() {
    XCTAssertEqual([].indexOfFirstObjectPassingTest {(_: Int) -> Bool in true}, 0)
    XCTAssertEqual([1].indexOfFirstObjectPassingTest {(_: Int) -> Bool in true}, 0)
    XCTAssertEqual([1,2].indexOfFirstObjectPassingTest {(_: Int) -> Bool in true}, 0)
    XCTAssertEqual([1,2,3].indexOfFirstObjectPassingTest {(_: Int) -> Bool in true}, 0)
    XCTAssertEqual([1,2,3,4].indexOfFirstObjectPassingTest {(_: Int) -> Bool in true}, 0)
  }

  func testFirstMatch() {
    XCTAssertEqual([1].indexOfFirstObjectPassingTest {1<=$0}, 0)
    XCTAssertEqual([0,1].indexOfFirstObjectPassingTest {1<=$0}, 1)
    XCTAssertEqual([1,2].indexOfFirstObjectPassingTest {1<=$0}, 0)
    XCTAssertEqual([0,1,2].indexOfFirstObjectPassingTest {1<=$0}, 1)
  }

  func testLots() {
    let a = Array(0..<1000)
    for i in a.indices {
      XCTAssertEqual(a.indexOfFirstObjectPassingTest({Int(i)<=$0}), i)
    }
  }
}
7
Benjohn

Voici une implémentation pour un tableau trié de chaînes. 

var arr = ["a", "abc", "aabc", "aabbc", "aaabbbcc", "bacc", "bbcc", "bbbccc", "cb", "cbb", "cbbc", "d" , "defff", "deffz"]

func binarySearch(_ array: [String], value: String) -> String {

    var firstIndex = 0
    var lastIndex = array.count - 1
    var wordToFind = "Not founded"
    var count = 0

    while firstIndex <= lastIndex {

        count += 1
        let middleIndex = (firstIndex + lastIndex) / 2
        let middleValue = array[middleIndex]

        if middleValue == value {
            wordToFind = middleValue
            return wordToFind
        }
        if value.localizedCompare(middleValue) == ComparisonResult.orderedDescending {
            firstIndex = middleIndex + 1
        }
        if value.localizedCompare(middleValue) == ComparisonResult.orderedAscending {
            print(middleValue)
            lastIndex = middleIndex - 1
        }
    }
    return wordToFind
}
//print d
print(binarySearch(arr, value: "d")) 
1
James Rochabrun
extension ArraySlice where Element: Comparable {
    func binarySearch(_ value: Element) -> Int? {
        guard !isEmpty else { return nil }

        let midIndex = (startIndex + endIndex) / 2
        if value == self[midIndex] {
            return midIndex
        } else if value > self[midIndex] {
            return self[(midIndex + 1)...].binarySearch(value)
        } else {
            return self[..<midIndex].binarySearch(value)
        }
    }
}

extension Array where Element: Comparable {
    func binarySearch(_ value: Element) -> Int? {
        return self[0...].binarySearch(value)
    }
}

Ceci, à mon avis, est très lisible et exploite le fait que Swift's ArraySlice est une vue sur Array et conserve les mêmes index que le tableau original avec lequel il partage le stockage. est donc très efficace.

1
Alessandro Martin

Voici un exemple complet avec plusieurs cas de test pour Swift 3.1. Il n'y a aucune chance que cela soit plus rapide que l'implémentation par défaut, mais ce n'est pas le but. L'extension du tableau est en bas:

//  BinarySearchTests.Swift
//  Created by Dan Rosenstark on 3/27/17
import XCTest
@testable import SwiftAlgos

class BinarySearchTests: XCTestCase {

    let sortedArray : [Int] = [-25, 1, 2, 4, 6, 8, 10, 14, 15, 1000]

    func test5() {
        let traditional = sortedArray.index(of: 5)
        let newImplementation = sortedArray.indexUsingBinarySearch(of: 5)
        XCTAssertEqual(traditional, newImplementation)
    }

    func testMembers() {
        for item in sortedArray {
            let traditional = sortedArray.index(of: item)
            let newImplementation = sortedArray.indexUsingBinarySearch(of: item)
            XCTAssertEqual(traditional, newImplementation)
        }
    }

    func testMembersAndNonMembers() {
        for item in (-100...100) {
            let traditional = sortedArray.index(of: item)
            let newImplementation = sortedArray.indexUsingBinarySearch(of: item)
            XCTAssertEqual(traditional, newImplementation)
        }
    }

    func testSingleMember() {
        let sortedArray = [50]
        for item in (0...100) {
            let traditional = sortedArray.index(of: item)
            let newImplementation = sortedArray.indexUsingBinarySearch(of: item)
            XCTAssertEqual(traditional, newImplementation)
        }
    }

    func testEmptyArray() {
        let sortedArray : [Int] = []
        for item in (0...100) {
            let traditional = sortedArray.index(of: item)
            let newImplementation = sortedArray.indexUsingBinarySearch(of: item)
            XCTAssertEqual(traditional, newImplementation)
        }
    }
}

extension Array where Element : Comparable {
    // self must be a sorted Array
    func indexUsingBinarySearch(of element: Element) -> Int? {
        guard self.count > 0 else { return nil }
        return binarySearch(for: element, minIndex: 0, maxIndex: self.count - 1)
    }

    private func binarySearch(for element: Element, minIndex: Int, maxIndex: Int) -> Int? {
        let count = maxIndex - minIndex + 1
        // if there are one or two elements, there is no futher recursion:
        // stop and check one or both values (and return nil if neither)
        if count == 1 {
            return element == self[minIndex] ? minIndex : nil
        } else if count == 2 {
            switch element {
                case self[minIndex]: return minIndex
                case self[maxIndex]: return maxIndex
                default: return nil
            }
        }

        let breakPointIndex = Int(round(Double(maxIndex - minIndex) / 2.0)) + minIndex
        let breakPoint = self[breakPointIndex]

        let splitUp = (breakPoint < element)
        let newMaxIndex : Int = splitUp ? maxIndex : breakPointIndex
        let newMinIndex : Int = splitUp ? breakPointIndex : minIndex

        return binarySearch(for: element, minIndex: newMinIndex, maxIndex: newMaxIndex)
    }
}

Ceci est assez fait maison, alors ... caveat emptor. Cela fonctionne et fait une recherche binaire.

0
Dan Rosenstark

Voici une meilleure implémentation qui renvoie plusieurs index, s'il y en a plusieurs dans le tableau.

extension Array where Element: Comparable {

/* Array Must be sorted */

func binarySearch(key: Element) -> [Index]? {
    return self.binarySearch(key, initialIndex: 0)
}

private func binarySearch(key: Element, initialIndex: Index) -> [Index]? {

    guard count > 0 else { return nil }

    let midIndex = count / 2
    let midElement = self[midIndex]

    if key == midElement {

        // Found!

        let foundIndex = initialIndex + midIndex

        var indexes = [foundIndex]

        // Check neighbors for same values

        // Check Left Side

        var leftIndex = midIndex - 1

        while leftIndex >= 0 {

            //While there is still more items on the left to check

            print(leftIndex)

            if self[leftIndex] == key {

                //If the items on the left is still matching key

                indexes.append(leftIndex + initialIndex)
                leftIndex--

            } else {

                // The item on the left is not identical to key

                break
            }
        }

        // Check Right side

        var rightIndex = midIndex + 1

        while rightIndex < count {

            //While there is still more items on the left to check

            if self[rightIndex] == key {

                //If the items on the left is still matching key

                indexes.append(rightIndex + initialIndex)
                rightIndex++

            } else {

                // The item on the left is not identical to key

                break
            }
        }

        return indexes.sort{ return $0 < $1 }
    }

    if count == 1 {

        guard let first = first else { return nil }

        if first == key {
            return [initialIndex]
        }
        return nil
    }


    if key < midElement {

        return Array(self[0..<midIndex]).binarySearch(key, initialIndex: initialIndex + 0)
    }

    if key > midElement {

        return Array(self[midIndex..<count]).binarySearch(key, initialIndex: initialIndex + midIndex)
    }

    return nil
}

}

0
Jacky Wang

voici une recherche binaire utilisant la syntaxe while 

func binarySearch<T: Comparable>(_ a: [T], key: T) -> Int? {
    var lowerBound = 0
    var upperBound = a.count
    while lowerBound < upperBound {
        let midIndex = lowerBound + (upperBound - lowerBound) / 2
        if a[midIndex] == key {
            return midIndex
        } else if a[midIndex] < key {
            lowerBound = midIndex + 1
        } else {
            upperBound = midIndex
        }
    }
    return nil
}
0
Lucy Jeong