Swift data structure - implementation of stack

  栈(Stack)It is a last-in-first-out (Last in First Out) data structure, which is only limited 栈顶to insert or delete operations. The practical application of stack structure mainly includes number system conversion, bracket matching, expression evaluation and so on. The schematic diagram of the stack data structure is as follows:

write picture description here

###1. Background knowledge From the above schematic diagram, we know that the stack is a restricted data structure. It is not random access like an array, and can only perform push and pop operations at the top of the stack, and The element pushed first is popped last, and the element pushed last is popped first. So the question is, in Swift, how do we implement a stack data structure? **Before answering this question, let's learn a little about the basics of Swift.

######1, value types and reference types

  A value type is always copied when it is assigned to another instance, or passed as a parameter to a function. That is to say, it assigns or transfers a copy, not the original, and you modify one of the values ​​without affecting the other. Structures and enumerations in Swift are both value types.

  When a reference type is assigned to another instance or passed as a parameter to a function, it does not make a copy, but creates a new reference to the underlying instance. That is, they all end up pointing to the same underlying instance, and as long as one reference modifies the instance's value, the other reference is also affected.

  There is no built-in stack type data structure in Swift, which means that if we want to implement a stack data structure, we must customize it. In Swift, both structs and classes can be used to define custom types , so should we use structs or classes? Before answering this question, let's take a brief look at the basic characteristics of structs and classes.

  In Objective-C programming, there is a big difference between a structure and a class. The decision to use a structure or a class can often be made at a glance. However, in Swift, structs are no longer as simple as they used to be, and many object-oriented features have been added to them, so that structs can perform tasks that only classes can perform. This seems to make the choice between structs and classes difficult. However, a struct is still a struct after all, it can never become a class. Therefore, for the choice between structure and class, the official still has certain guiding principles:

(1) If the type needs to be passed by value, then use the structure. Doing so will ensure that the type is copied when assigning or passing parameters to the function; (2) If the type does not support, or it is not necessary to support subclass inheritance, then use the structure. Structure does not support inheritance relationship, so there is no subclass problem; (3) If the behavior to be expressed by the type is relatively intuitive, and it only contains some simple values, then it is better to consider using structure. If necessary, you can turn it into a class at any time; (4) Last point, the structure belongs to the value type, and the class belongs to the reference type. If you need to ensure that the data is independent from other influences, it is best to use a structure; (5) In all other cases except the above 4 points, it is recommended to use a class.

  After reading the above selection suggestions, I believe that when you implement the stack structure, whether you should choose a structure or a class, you must have a clear answer in your heart. Because the stack is mainly used to store data and does not involve inheritance, and the main operations are stacking and popping, it is best to use a structure.

######2. Know the built-in array types in Swift

  After determining the type of structure used by the stack, the next step is to find backing storage for the stack. To this end, let’s review some basic characteristics of the stack data structure: ①, the stack can store multiple data elements , so its backing memory must be able to store multiple independent data at a time; ②, in addition, the stack is a last-in-first In other words, the elements in the stack must maintain a certain order when they are pushed and popped from the stack. The built-in types that already exist in Swift can satisfy the above two points, basically only 元组(Tuple)sum 数组(Array). However, the operation of tuples is not as flexible as that of arrays. More importantly, there are many built-in properties and methods in arrays, which can greatly help us simplify the implementation process of stack structure. For this purpose, using 数组(Array)a backing store as a stack is obviously the best option.

  In the implementation process of the stack data structure, we have determined the overall direction, that is, to adopt the structure type and use it 数组(Array)as a backing storage. Next, in order to better understand the implementation process of the stack data structure, you can first understand some 数组(Array)commonly used built-in properties and methods. For related content, please refer to the previous note "Introduction to Swift Data Structures" , and you can also refer to "The Swift Programming" Contents of Collection Types in Language" .

######3, Generics, Where and Protocol Oriented Programming

  We all know that constants, variables, and functions in Swift have their own types. When declaring them, you must specify the type (or initialize them through literal syntax, and then have the system perform type inference), and the type Once a period is determined, only the same type of data can be stored. But in many cases, especially functions, the specific implementation process is exactly the same, but the data types processed are different. In this way, it is easy to generate such a problem: in order to process as many data types as possible, the same function needs to be written many times, which is obviously not a smart approach. In order to deal with this problem, the concept of generics is introduced in Swift, that is, when declaring a function, it does not specify the specific data type first, but uses placeholders, and then determines its real data when the specific data is actually used. data type . The advantage of this is that it can greatly increase the flexibility of the code and minimize the repetitive code.

  The syntax for declaring generics in Swift is to follow the type name with a pair 尖括号(<>), and inside the angle brackets, use a specific symbol as the name 类型占位符. A placeholder can be any letter or a string, but usually we use an uppercase letter Tas a type placeholder:

/// 泛型结构体,其处理的数据类型是泛型T
struct Stack<T> {
	// 代码...
}

/// 泛型函数和方法(拥有两个泛型占位符)
///
/// - Parameter items: 参数items的类型是一个泛型数组[T]
/// - Parameter f: 参数f是一个闭包,其参数是一个泛型T,返回值是一个泛型U
/// - Returns: 整个函数的返回值是一个泛型数组[U]
func myMap<T, U>(_ items: [T], _ f: (T) -> (U)) -> [U] { ... }

  In addition to generics, there is another very important concept we will use later, that is 类型约束(Type Constraint). We talked about generics above, but type constraints are closely related to generics. Because the real type of the generic is not known until the actual application, and before that, we know nothing about the type of data that is about to be used. In this way, it is easy to cause a problem that no matter what type of data can be passed in. But sometimes not all types of data are suitable. For example, if a function is used to compare the size of two parameter values, then the two parameter values ​​must comply with the Equatableprotocol, otherwise it will not pass when compiling. 类型约束To solve this problem, the concept of Swift was introduced .

  Type constraints in Swift refer to some necessary restrictions on the specific types of parameters when passing parameters to generic functions. There are two cases of type constraints: the first case is that the type of the passed parameter must be a subclass of a specified class; the other case is that the passed parameter must conform to a certain protocol, or a combination of protocols. The syntax of the type constraint is to follow the generic type 冒号(:), and then write a subclass of the specified class, or a protocol (composition). Take the comparison of two parameter values ​​for equality as an example to demonstrate type constraints:

/// 比较两个值是否相等。所传参数的类型是一个泛型,并且参数必须遵守Equatable协议
///
/// - Parameter firstValue: 第一个参数,参数必须遵守Equatable协议
/// - Parameter secondValue: 第二个参数,参数必须遵守Equatable协议
/// - Returns: 如果所传两个参数相等,则返回true,否则返回false
func checkIfEqual<T: Equatable>(_ firstValue: T, _ secondValue: T) -> Bool {
    return firstValue == secondValue
}

  Another very important thing is keywords where. where can be used for both conditional filtering and type constraints. As far as the above type constraints are concerned, it is equivalent to the following:

func checkIfEqual<T>(_ firstValue: T, _ secondValue: T) -> Bool where T: Equatable {
    return firstValue == secondValue
}

  If you want to know more about the usage of where, you can refer to Apple's official documentation. The last knowledge point is protocol-oriented programming. You can watch the official Protocol-Oriented Programming in Swift and What the 55 Swift Standard Library Protocols Taught Me of Swift Summit .

### Second, the basic implementation of the stack structure

  Let's first implement a basic stack data structure. The structure must have the ability to store data elements, and most importantly, the ability to pop and push the stack. To do this, first declare an array to store data elements, and then implement some basic operation methods:

/// 实现一个基本的栈结构
struct Stack<T> {
    
    /// 声明一个泛型数组,用于存储栈中的元素(栈结构的后备存储器)
    private var elements = [T]()
    
    /// 返回栈结构中元素的个数
    public var count: Int {
        
        // 返回数组elements中的元素个数
        return elements.count
    }
    
    /// 获取或者设置栈的存储容量
    public var capacity: Int {
        
        // 获取栈的存储容量
        get {
            
            // 返回数组elements的容量
            return elements.capacity
        }
        
        // 设置栈的最小容量
        set {
            
            // 设置数组elements的最小容量
            elements.reserveCapacity(newValue)
        }
    }
    
    /// 初始化方法(创建栈实例)
    public init() {}
    
    /// 使用push方法执行入栈操作
    public mutating func push(element: T) {
        
        // 判断栈是否已满
        if count == capacity {
            fatalError("栈已满,不能再执行入栈操作!")
        }
        
        // 使用数组的append()方法将元素添加到数组elements中
        self.elements.append(element)
    }
    
    /// 使用pop方法执行出栈操作
    @discardableResult
    public mutating func pop() -> T? {
        
        // 判断栈是否已空
        if count == 0 {
            fatalError("栈已空,不能再执行出栈操作!")
        }
        
        // 移除数组elements的最后一个元素
        return elements.popLast()
    }
    
    /// 返回栈顶元素
    public func peek() -> T? {
        
        // 返回数组elements的最后一个元素(但是不移除该元素)
        return elements.last
    }
    
    /// 清空栈中所有的元素
    public mutating func clear() {
        
        // 清空数组elements中所有的元素
        elements.removeAll()
    }
    
    /// 判断栈是否为空
    public func isEmpty() -> Bool {
        
        // 判断数组elements是否为空
        return elements.isEmpty
    }
    
    /// 判断栈是否已满
    public func isFull() -> Bool {
        
        // 对数组的存储情况进行判断
        if count == 0 {
            
            // 如果数组为空,则表示栈还未存储数据元素
            return false
        } else {
            
            // 如果数组不为空,则返回数组的存储容量
            // 然后再根据实际存储情况判断栈是否已满
            return count == elements.capacity
        }
    }
}

  Regarding the above code, the comments are very clearly written. If you have reviewed the relevant knowledge of arrays in Swift, I believe it is not difficult to understand. Briefly say keywords mutatingand features @discardableResult. As we said earlier, the structure is a value type, and the method of the value type cannot modify itself (self). If you have to modify the self (self), you must mutatingmark it with a keyword. push(element: )Because methods and methods in the Stack type pop()need to modify the number of elements in the stack, they need to mutatingbe marked with . @discardableResultis a feature in Swift to suppress warnings when a function or method return value is called without using its result. If you are not particularly sensitive to warnings, consider not using this feature.

  In order to check whether the stack we designed can work normally, you can copy the above code to your own project for testing, or you can download the project from my code repository for testing.

###Three, the extension of the stack function

  Our stack structure only has some simple functions, which can be extended by complying with relevant protocols. First follow the CustomStringConvertibleagreement CustomDebugStringConvertibleand make the effect of printing Stack instances more concise and beautiful:

// MARK: - 遵守CustomStringConvertible和CustomDebugStringConvertible协议
extension Stack: CustomStringConvertible, CustomDebugStringConvertible {

    // 为Stack增加description属性,实现打印简洁漂亮的格式
    // 未遵守协议之前打印效果:Stack<String>(elements: ["刘备", "关羽", "张飞"])
    // 遵守协议之后的打印效果:["刘备", "关羽", "张飞"]
    public var description: String {

        // 返回数组elements的文本表示
        return elements.description
    }
    
    // 为Stack增加description属性,实现打印简洁漂亮的格式,适配debug模式
    // 未遵守协议之前打印效果:Stack<String>(elements: ["刘备", "关羽", "张飞"])
    // 遵守协议之后的打印效果:["刘备", "关羽", "张飞"]
    public var debugDescription: String {
        
        // 返回数组elements的文本表示,适配debug模式
        return elements.debugDescription
    }
}

  When creating a Stack instance, the usage var stringStack: Stack<String> = Stack()or var intStack = Stack<Int>()syntax may seem too verbose. If you want to use var doubleStack: Stack = [1.0, 2.0, 3.0]literal syntax like an array, you must follow the ExpressibleByArrayLiteralprotocol and implement the corresponding method:


// MARK: - 遵守ExpressibleByArrayLiteral协议
extension Stack: ExpressibleByArrayLiteral {
    
    // 为Stack增加类似于数组的字面量初始化方法
    public init(arrayLiteral elements: T...) {
        self.init()
    }
}

  If you want the Stack type to support for...inand forEach(_ body: )syntax, you should conform to the Sequenceprotocol and implement the corresponding methods:

// MARK: - 遵守IteratorProtocol协议(手动实现迭代器功能)
public struct ArrayIterator<T> : IteratorProtocol {
    var currentElement: [T]
    init(elements: [T]) {
        self.currentElement = elements
    }

    mutating public func next() -> T? {
        if (!self.currentElement.isEmpty) {
            return self.currentElement.popLast()
        }
        return nil
    }
}

// MARK: - 遵守Sequence协议(实现for...in循环)
extension Stack: Sequence {
    public func makeIterator() -> ArrayIterator<T> {
        return ArrayIterator<T>(elements: self.elements)
    }
}

/****************** 或者直接实现下面这一个方法 ******************/

// MARK: - 遵守Sequence协议
extension Stack: Sequence {

    // 实现for...in循环和forEach循环
    public func makeIterator() -> AnyIterator<T> {
        return AnyIterator(IndexingIterator(_elements: self.elements.lazy.reversed()))
    }
}

  Finally, add a new constructor, which allows us to take an existing instance as a parameter, and then create another completely new instance:

// MARK: - 增加新的构造函数
extension Stack {
    
    /// 实现用一个已经存在的栈作为参数,然后初始化一个新的栈
    ///
    /// - Parameter stack: 一个已经存在的栈实例
    public init<S: Sequence>(_ stack: S) where S.Iterator.Element == T {
        
        // 将一个已经存在的栈实例转置,然后作为参数传入
        self.elements = Array(stack.reversed())
    }
}

  The above is the implementation process of a simple stack structure. You can try it yourself, and you will continue to learn other types of data structures later. See SwiftBooks for the complete code .

###4. References

[1] Matthew Mathias. John Gallagher. "Swift Programming The Big Nerd Ranch Guide (2nd Edition)" [2] Yan Weimin. Wu Weimin. "Data Structures (C Language Edition)" [3] Erik Azar. Mario Eguiluz Alebicto. "Swift" Data Structure and Algorithms

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324980024&siteId=291194637