An array is an ordered collection of elements of the same type, and they are used for pretty much anything that requires storing things in a certain order, such as the contents of lists in apps. It works like similar types in other languages.

Follow these steps to work with arrays:

Here are their abilities, represented by some of the protocols they conform to:

Workingarraysworking with with Arrays

The index type of an Array is Int (integer), and its startIndex is always 0. Its endIndex is the same as the length of the array. You can think of an index as something that's pointing to the space between elements, right before the element it refers to. Here is an array of characters:

Index

endIndex points to the position after the end, so if you ever try to access an element at endIndex with characters[characters.endIndex] (or with any other invalid index), your program will crash. If an array is empty, startIndex and endIndex are both 0.

All sequences have a SubSequence, a type which represents a subrange of its elements. The Array.SubSequence is an ArraySlice:

ArraySlice

It has the same heritage and API as Array. It keeps a reference to the array it was created from, and its startIndex and endIndex represent the subrange within the array:

ArraySlice

This allows us to have just one copy of a big array in memory, and have as many slices as there are views on it. However, each slice holds on to the array, so if you want to keep a slice around for a while, it is recommended to convert it to an array (using Array(slice)). This will copy the elements of the slice to its own array and release the reference, allowing the big array to be freed if nothing else holds on to it.

If you mutate the array or the slice after the slice has been created, a copy will be made automatically and the change will not be reflected in the other.

Many operations on arrays can be done far more efficiently if the array is sorted. We will add methods that take advantage of this for insertion, finding the index of the first or last occurrence of an element, and checking if the array contains an element.

We will just add methods to an array in an extension, but ideally this should be its own type with an internal array so that we can guarantee that it is always sorted. Check out ole/SortedArray (https://github.com/ole/SortedArray) for an example of this.

To perform basic array operations such as, inserting elements into an array and searching an element in an array.

  1. Open the CollectionsExtra Xcode project, and go to SortedArray.swift.
  2. Create an extension to Range to find the middle of it. This will be used with the indices of the array:
    public extension Range where Bound == Int {
      /// The value in the middle of this range. Returns nil if the range is empty.
      var middle: Int? {
        guard !isEmpty else { return nil }
        return lowerBound + count / 2
      }
    }
  3. We will assume that the array has been sorted using the < operator (ascending), and we will assure that elements can be used with this operator by constraining the extension to arrays with elements that adopt the Comparable protocol. This also means they can be used with >, ==, !=, >=, and <=:
    extension Array where Element: Comparable {
  4. Next, we need to find the insertion point if we were to insert an element into the sorted array. We can use this for insertion and checking if the array contains a specific element. This is a standard binary search, implemented with recursion:
      /// The index to use if you were to insert this element into a sorted array.
      ///
      /// - Parameters:
      ///   - element: The element to potentially insert.
      ///   - range: The range to search in.
      /// - Note: If the element already occurs once or more, the index to one of those will be returned.
      func insertionIndex(for element: Element, in range: Range<Index>) -> Index {
        guard let middle = range.middle else { return range.upperBound }
        if self[middle] < element {
          return insertionIndex(for: element, in: index(after: middle)..<range.upperBound)
        } else if self[middle] > element {
          return insertionIndex(for: element, in: range.lowerBound..<middle)
        }
        return middle
      }

    Note that when returning middle, we do not check if the element in that position is the one we are searching for. This is because the Comparable protocol demands that if an element is neither bigger than or smaller than another element, then they must be equal.

    The range will normally start as the entire array.

  5. Inserting an element is now very simple:
      /// Inserts the element in the correct position in a sorted array.
      ///
      /// - Parameter element: The element to insert.
      /// - Returns: The index where the element was inserted.
      @discardableResult
      public mutating func sorted_insert(_ element: Element) -> Index {
        let index = insertionIndex(for: element, in: startIndex..<endIndex)
        self.insert(element, at: index)
        return index
      }
  6. When checking if the array contains a specific element, we can first get the insertion index, check that it is not the endIndex (if the element does not exist and is larger than all the other elements), and see if the element at the index is the one we are searching for:
       /// Checks if a sorted array contains an element.
      public func sorted_contains(_ element: Element) -> Bool {
        let index = insertionIndex(for: element, in: startIndex..<endIndex)
        return (index != endIndex) && (self[index] == element)
      }
  7. When searching for the first occurrence of an element in the array, we can't use insertionIndex. This is because if the element occurs more than once, it may return the index to any of those occurrences. Instead, we will use a slightly modified version (https://github.com/raywenderlich/swift-algorithm-club/blob/master/Count%20Occurrences/README.markdown):
      /// The index of the first occurrence of this element in a sorted array.
      ///
      /// - Parameters:
      ///   - element: The element to search for.
      ///   - range: The range to search within.
      /// - Returns: The index, or nil if not found.
      public func sorted_index(of element: Element, in range: Range<Index>? = nil) -> Index? {
        let range = range ?? startIndex..<endIndex
        guard let middle = range.middle else {
          let index = range.upperBound
          return (self.indices.contains(index) && self[index] == element) ? index : nil
        }
        if self[middle] < element {
          return sorted_index(of: element, in: index(after: middle)..<range.upperBound)
        }
        return sorted_index(of: element, in: range.lowerBound..<middle)
      }

    The main difference is that we only check if the element in the middle is less than what we are searching for, not both less than and greater than, like in insertionIndex. We can do this because, in a sorted array, all equal elements are grouped together. Even if middle happens to point to an equal element, there may still be more of those to the left, so we continue searching there. If there aren't, we still end up with the index in the correct place.

    Since we are using properties of self for the default value of the range parameter, we cannot provide them in the function header. Instead, we set the default value to nil, and then create a new local variable called range which is set to the default value startIndex..<endIndex if no other value was provided when the function was called.

  8. The code for finding the last index of an element is almost identical:
      /// The index of the last occurrence of this element in a sorted array.
      ///
      /// - Parameters:
      ///   - element: The element to search for.
      ///   - range: The range to search within.
      /// - Returns: The index, or nil if not found.
      public func sorted_lastIndex(of element: Element, in range: Range<Index>? = nil) -> Index? {
        let range = range ?? startIndex..<endIndex
        guard let middle = range.middle else {
          let index = self.index(before: range.upperBound)
          return (self.indices.contains(index) && self[index] == element) ? index : nil
        }
        if self[middle] > element {
          return sorted_lastIndex(of: element, in: range.lowerBound..<middle)
        }
        return sorted_lastIndex(of: element, in: index(after: middle)..<range.upperBound)
      }
    }

    Here, we check if middle points to an element that is greater than what we are searching for. If it isn't, we go to the right. When we have finally found an index, we use the index before it.

  9. Go to SortedArrayTests.swift, uncomment the unit tests, and run them all.