多種語言實現數據結構之稀疏數組和隊列

前言

一部分有工做經驗的老司機對數據結構是很熟悉了,而一部分剛參加工做或者剛入行的人對數據結構是略懂一二甚至是感到陌生,但願本篇文章可讓老司機更熟悉數據結構的實現,不懂數據結構的小白對數據結構的實現有必定的瞭解。javascript

本系列文章使用多種語言實現經常使用的數據結構,包括目前使用最多的 Java,21 世紀興起的 Go,前端領域的 JavaScript,目的是儘量的讓更多的人可以看的懂、看的明白。總體上採用文字、圖和代碼的方式進行介紹,代碼默認使用 Go 語言版本,其它語言請參考完整的源代碼,對應的源代碼見下文。前端

全部已經更新的內容在 Github 上,若是對你有幫助請給個 star,謝謝 \ (•◡•) /,你的確定是我繼續更新的動力 ☺。java

稀疏數組

各類語言實現代碼:Go Java JavaScriptgit

默認使用 Go 語言實現。github

介紹

在二維數組中,若是值爲 0 的元素數目遠遠大於非 0 元素的數目,而且非 0 元素的分佈沒有規律,則該數組被稱爲稀疏數組。若是非 0 元素數目佔大多數,則稱該數組爲稠密數組。數組的稠密度指的是非零元素的總數比上數組全部元素的總數。golang

下圖是一個 0 值遠大於非 0 值的二維數組後端

多種語言實現數據結構之稀疏數組和隊列

稀疏數組能夠看作是一個壓縮的數組,稀疏數組的好處有:數組

  • 原數組中存在大量的無效數據,佔據了大量的存儲空間,真正有用的數據卻少之又少
  • 壓縮存儲能夠節省存儲空間以免資源的沒必要要的浪費,在數據序列化到磁盤時,壓縮存儲能夠提升 IO 效率

採用稀疏數組的存儲方式爲第一行存儲原始數據總行數,總列數,默認值 0,接下來每一行都存儲非0數所在行,所在列,和具體值。上圖中的二維數組轉成稀疏數組後以下:數據結構

多種語言實現數據結構之稀疏數組和隊列

下面使用稀疏數組存儲上述的二維數組,把稀疏數組保存在文件中,而且能夠從新恢復成二維數組。app

建立二維數組並初始化

func printArray(array [5][5]int) {
    for i := 0; i < len(array); i++ {
        for j := 0; j < len(array[i]); j++ {
            fmt.Print(array[i][j], "\t")
        }
        fmt.Print("\n")
    }
}

func main() {
    // 定義一個二維數組
    var array [5][5]int
    // 初始化 3,6, 1,5
    array[0][2] = 3
    array[1][3] = 6
    array[2][1] = 1
    array[3][3] = 5

    fmt.Println("原二維數組:")
    printArray(array)
}

輸出:

原二維數組:
0   0   3   0   0   
0   0   0   6   0   
0   1   0   0   0   
0   0   0   5   0   
0   0   0   0   0

二維數組轉稀疏數組

// 二維數組轉稀疏數組
func toSparseArray(array [5][5]int) [][3]int {
    // Go 語言這樣寫沒法編譯經過
    // var sparseArray [count + 1][]int
    // 使用切片來定義
    var sparseArray = make([][3]int, 0)
    sparseArray = append(sparseArray, [3]int{ 5, 5, 0})

    for i := 0; i < len(array); i++ {
        for j := 0; j < len(array[i]); j++ {
            if array[i][j] != 0 {
                // 保存 row, col, val
                sparseArray = append(sparseArray, [3]int{ i, j, array[i][j]})
            }
        }
    }

    return sparseArray
}

func printArray(array [5][5]int) {
    for i := 0; i < len(array); i++ {
        for j := 0; j < len(array[i]); j++ {
            fmt.Print(array[i][j], "\t")
        }
        fmt.Print("\n")
    }
}

func printSparseArray(sparseArray [][3]int) {
    for i := 0; i < len(sparseArray); i++ {
        for j := 0; j < 3; j++ {
            fmt.Print(sparseArray[i][j], "\t")
        }
        fmt.Print("\n")
    }
}

func main() {
    // 定義一個二維數組
    var array [5][5]int
    // 初始化 3,6, 1,5
    array[0][2] = 3
    array[1][3] = 6
    array[2][1] = 1
    array[3][3] = 5

    fmt.Println("原二維數組:")
    printArray(array)

    // 轉成稀疏數組
    sparseArray := toSparseArray(array)

    fmt.Println("轉換後的稀疏數組:")
    printSparseArray(sparseArray)
}

輸出:

原二維數組:
0   0   3   0   0   
0   0   0   6   0   
0   1   0   0   0   
0   0   0   5   0   
0   0   0   0   0   
轉換後的稀疏數組:
5   5   0   
0   2   3   
1   3   6   
2   1   1   
3   3   5

存儲和讀取稀疏數組

var sparseArrayFileName = "./sparse.data"
// 存儲稀疏數組
func storageSparseArray(sparseArray [][3]int) {
    file, err := os.Create(sparseArrayFileName)
    defer file.Close()

    if err != nil {
        fmt.Println("建立文件 sparse.data 錯誤:", err)
        return
    }
    // 存儲矩陣格式
    for i := 0 ; i < len(sparseArray); i++ {
        content := ""
        for j := 0; j < 3; j++ {
            content += strconv.Itoa(sparseArray[i][j]) + "\t"
        }
        // 行分隔符
        content += "\n"
        _, err = file.WriteString(content)
        if err != nil {
            fmt.Println("寫入內容錯誤:", err)
        }
    }
}

// 讀取稀疏數組
func readSparseArray() [][3]int {
    file, err := os.Open(sparseArrayFileName)
    defer file.Close()

    if err != nil {
        fmt.Println("打開文件 sparse.data 錯誤:", err)
        return nil
    }
    sparseArray := make([][3]int, 0)
    reader := bufio.NewReader(file)
    for {
        // 分行讀取
        content, err := reader.ReadString('\n')
        if err == io.EOF {
            break
        }
        arr := strings.Split(content, "\t")
        row, _ := strconv.Atoi(arr[0])
        col, _ := strconv.Atoi(arr[1])
        val, _ := strconv.Atoi(arr[2])
        sparseArray = append(sparseArray, [3]int { row, col, val})
    }
    return sparseArray
}

func main() {
    // 定義一個二維數組
    var array [5][5]int
    // 初始化 3,6, 1,5
    array[0][2] = 3
    array[1][3] = 6
    array[2][1] = 1
    array[3][3] = 5

    fmt.Println("原二維數組:")
    printArray(array)

    // 轉成稀疏數組
    sparseArray := toSparseArray(array)

    fmt.Println("轉換後的稀疏數組:")
    printSparseArray(sparseArray)

    // 存儲稀疏數組
    storageSparseArray(sparseArray)

    // 讀取稀疏數組
    sparseArray = readSparseArray()
    fmt.Println("讀取的稀疏數組:")
    printSparseArray(sparseArray)
}

運行以上代碼後,打開 sparse.data 文件,內容以下:

5   5   0   
0   2   3   
1   3   6   
2   1   1   
3   3   5

輸出:

...

讀取的稀疏數組:
5   5   0   
0   2   3   
1   3   6   
2   1   1   
3   3   5

稀疏數組轉二維數組

// 稀疏數組轉二維數組
func toArray(sparseArray [][3]int) [5][5]int {
    var array [5][5]int

    // 從稀疏數組中第二行開始讀取數據
    for i := 1; i < len(sparseArray); i++ {
        row := sparseArray[i][0]
        col := sparseArray[i][1]
        val := sparseArray[i][2]
        array[row][col] = val
    }

    return array
}

func main() {
    // 定義一個二維數組
    var array [5][5]int
    // 初始化 3,6, 1,5
    array[0][2] = 3
    array[1][3] = 6
    array[2][1] = 1
    array[3][3] = 5

    fmt.Println("原二維數組:")
    printArray(array)

    // 轉成稀疏數組
    sparseArray := toSparseArray(array)

    fmt.Println("轉換後的稀疏數組:")
    printSparseArray(sparseArray)

    // 存儲稀疏數組
    storageSparseArray(sparseArray)

    // 讀取稀疏數組
    sparseArray = readSparseArray()
    fmt.Println("讀取的稀疏數組:")
    printSparseArray(sparseArray)

    // 轉成二維數組
    array = toArray(sparseArray)
    fmt.Println("轉換後的二維數組:")
    printArray(array)
}

輸出:

...

轉換後的二維數組:
0   0   3   0   0   
0   0   0   6   0   
0   1   0   0   0   
0   0   0   5   0   
0   0   0   0   0

隊列(queue)

各類語言實現代碼:Go Java JavaScript

默認使用 Go 語言實現。

介紹

隊列是一種特殊的線性表,它只容許在線性表的前端進行刪除操做,在表的後端進行插入操做,因此隊列又稱爲先進先出(FIFO—first in first out)的線性表。進行插入操做的一端叫作隊尾,進行刪除操做的一端叫作隊頭。隊列的數據元素叫作隊列元素,在隊列中插入一個隊列元素稱爲入隊,從隊列中刪除一個隊列元素稱爲出隊。

多種語言實現數據結構之稀疏數組和隊列

順序隊列

順序隊列相似數組,它須要一塊連續的內存,並有兩個指針,一個是隊頭指針 front,它指向隊頭元素,另外一個是隊尾指針 rear,它指向下一個入隊的位置。在隊尾插入一個元素時,隊尾指針加一,在隊頭刪除一個元素時,隊頭指針加一。不斷的進行插入和刪除操做,隊列元素在不斷的變化,當隊頭指針等於隊尾指針時,隊列中沒有任何元素,沒有元素的隊列稱爲空隊列。對於已經出隊列的元素所佔用的空間,順序隊列沒法再次利用。

隊列能夠使用數組結構或者鏈表結構來實現,這裏使用數組來實現隊列。

用數組來實現順序隊列的思路:

  • 定義數組,存儲隊列元素
  • 定義隊列最大大小 maxSize
  • 定義隊頭指針 front,初始化爲 0
  • 隊尾指針 rear,初始化爲 0
  • 入隊方法 put
  • 出隊方法 take

定義順序隊列結構體和建立結構體實例函數:

type IntQueue struct {
    array []int // 存放隊列元素的切片(數組沒法使用變量來定義長度)
    maxSize int // 最大隊列元素大小
    front int // 隊頭指針
    rear int // 隊尾指針
}

func NewQueue(size int) *IntQueue {
    return &IntQueue{
        array:   make([]int, size),
        maxSize: size,
        front:   0,
        rear:    0,
    }
}

入隊方法:

func (q *IntQueue) Put(elem int) error {
    // 隊尾指針不能超過最大隊列元素大小
    if q.rear >= q.maxSize {
        return errors.New("queue is full")
    }
    q.array[q.rear] = elem
    q.rear++ // 隊尾指針加一
    return nil
}

出隊方法:

func (q *IntQueue) Take() (int, error) {
    // 隊頭指針等於隊尾指針表示隊列爲空
    if q.front == q.rear {
        return 0, errors.New("queue is empty")
    }
    elem := q.array[q.front]
    q.front++ // 隊頭指針加一
    return elem, nil
}

爲了方便查看輸出結果,從新定義 String 方法:

// 從新定義 String 方法,方便輸出
func (q *IntQueue) String() string {
    str := "["
    for i := q.front; i < q.rear; i++ {
        str += strconv.Itoa(q.array[i]) + " "
    }
    str += "]"
    return str
}

測試代碼以下:

func main() {
    intQueue := NewQueue(3)
    _ = intQueue.Put(1)
    _ = intQueue.Put(2)
    _ = intQueue.Put(3)
    _ = intQueue.Put(4) // 隊列已滿,沒法放入數據

    fmt.Println("intQueue:", intQueue)

    num, _ := intQueue.Take()
    fmt.Println("取出一個元素:", num)
    num, _ = intQueue.Take()
    fmt.Println("取出一個元素:", num)
    num, _ = intQueue.Take()
    fmt.Println("取出一個元素:", num)
    num, takeErr := intQueue.Take()
    fmt.Println("取出一個元素:", num)
    if takeErr != nil {
        fmt.Println("出隊失敗:", takeErr)
    }

    // 此時隊列已經用完,沒法放數據
    putErr := intQueue.Put(4)
    if putErr != nil {
        fmt.Println("入隊失敗:", putErr)
    }
    fmt.Println("intQueue:", intQueue)
}

測試以上代碼,輸出:

intQueue: [1 2 3 ]
取出一個元素: 1
取出一個元素: 2
取出一個元素: 3
取出一個元素: 0
出隊失敗: queue is empty
入隊失敗: queue is full
intQueue: []

循環隊列

在實際使用隊列時,順序隊列空間不能重複使用,須要對順序隊列進行改進。無論是插入或刪除,一旦 rear 指針增 1 或 front 指針增 1 時超出了隊列所分配的空間,就讓它指向起始位置。當 MaxSize - 1增 1變到 0,可用取餘運算獲取隊頭或者隊尾指針增長 1 後的位置,隊尾指針計算方法爲 rear % MaxSize,隊尾指針計算方法爲 front % MaxSize。這種循環使用隊列空間的隊列稱爲循環隊列。除了一些簡單應用以外,真正實用的隊列是循環隊列。

使用數組實現循環隊列思路:

  • 定義數組,存儲隊列元素
  • 定義隊列最大大小 maxSize
  • 定義隊頭指針 front,初始化爲 0,刪除元素後,從新計算值,計算公式爲:(front + 1) % maxSize
  • 隊尾指針 rear,初始化爲 0,插入元素後,從新計算值,計算公式爲:(q.rear + 1) % q.maxSize
  • 判斷隊列是否爲空的方法,判斷 front 等於 rear 判斷便可
  • 判斷隊列是否已滿的方法,判斷 (rear + 1) % maxSize 是否等於 front
  • 獲取隊列元素大小方法,計算公式:(rear + maxSize - front) % maxSize
  • 入隊方法 put
  • 出隊方法 take

定義循環隊列結構體和建立結構體實例函數:

type IntQueue struct {
    array []int // 存放隊列元素的切片(數組沒法使用變量來定義長度)
    maxSize int // 最大隊列元素大小
    front int // 隊頭指針
    rear int // 隊尾指針
}

func NewQueue(size int) *IntQueue {
    return &IntQueue{
        array:   make([]int, size),
        maxSize: size,
        front:   0,
        rear:    0,
    }
}

判斷隊列是否爲空:

func (q *IntQueue) isEmpty() bool {
    // 隊頭指針等於隊尾指針表示隊列爲空
    return q.front == q.rear
}

判斷隊列是否已滿:

func (q *IntQueue) isFull() bool {
    // 空出一個位置,判斷是否等於隊頭指針
    // 隊尾指針指向的位置不能存放隊列元素,實際上會比 maxSize 指定的大小少一
    return (q.rear + 1) % q.maxSize == q.front
}

獲取隊列元素大小:

func (q *IntQueue) size() int {
    return (q.rear + q.maxSize - q.front) % q.maxSize
}

入隊方法:

func (q *IntQueue) Put(elem int) error {
    if q.isFull() {
        return errors.New("queue is full")
    }
    q.array[q.rear] = elem
    // 循環累加,當 rear + 1 等於 maxSize 時變成 0,從新累加
    q.rear = (q.rear + 1) % q.maxSize
    return nil
}

出隊方法:

func (q *IntQueue) Take() (int, error) {
    if q.isEmpty() {
        return 0, errors.New("queue is empty")
    }
    elem := q.array[q.front]
    q.front = (q.front + 1) % q.maxSize
    return elem, nil
}

從新定義的 String 方法:

func (q *IntQueue) String() string {
    str := "["
    tempFront := q.front
    for i := 0; i < q.size(); i++ {
        str += strconv.Itoa(q.array[tempFront]) + " "
        // 超過最大大小,從 0 開始
        tempFront = (tempFront + 1 ) % q.maxSize
    }
    str += "]"
    return str
}

測試代碼以下:

func main() {
    intQueue := NewQueue(5)
    _ = intQueue.Put(1)
    _ = intQueue.Put(2)
    _ = intQueue.Put(3)
    _ = intQueue.Put(4)
    _ = intQueue.Put(5) // 隊列已滿,沒法放入數據,實際上只能放 4 個元素

    fmt.Println("intQueue:", intQueue)

    num, _ := intQueue.Take()
    fmt.Println("取出一個元素:", num)
    num, _ = intQueue.Take()
    fmt.Println("取出一個元素:", num)
    num, _ = intQueue.Take()
    fmt.Println("取出一個元素:", num)
    num, _ = intQueue.Take()
    fmt.Println("取出一個元素:", num)
    num, takeErr := intQueue.Take()
    fmt.Println("取出一個元素:", num)
    if takeErr != nil {
        fmt.Println("出隊失敗:", takeErr)
    }

    // 取出數據後能夠繼續放入數據
    _ = intQueue.Put(5)
    fmt.Println("intQueue:", intQueue)
}

測試以上代碼,輸出:

intQueue: [1 2 3 4 ]
取出一個元素: 1
取出一個元素: 2
取出一個元素: 3
取出一個元素: 4
取出一個元素: 0
出隊失敗: queue is empty
intQueue: [5 ]
相關文章
相關標籤/搜索