数据结构与算法|青训营笔记

55 阅读5分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第5篇笔记

数据结构与算法

常见的数据结构
  • **栈(Stack):**栈是一种特殊的线性表,它只能在一个表的一个固定端进行数据结点的插入和删除操作。
  • **队列(Queue):**队列和栈类似,也是一种特殊的线性表。和栈不同的是,队列只允许在表的一端进行插入操作,而在另一端进行删除操作。
  • **数组(Array):**数组是一种聚合数据类型,它是将具有相同类型的若干变量有序地组织在一起的集合。
  • **链表(Linked List):**链表是一种数据元素按照链式存储结构进行存储的数据结构,这种存储结构具有在物理上存在非连续的特点。
  • **树(Tree):**树是典型的非线性结构,它是包括,2 个结点的有穷集合 K。
  • **图(Graph):**图是另一种非线性数据结构。在图结构中,数据结点一般称为顶点,而边是顶点的有序偶对。
  • **堆(Heap):**堆是一种特殊的树形数据结构,一般讨论的堆都是二叉堆。
  • **散列表(Hash table):**散列表源自于散列函数(Hash function),其思想是如果在结构中存在关键字和T相等的记录,那么必定在F(T)的存储位置可以找到该记录,这样就可以不用进行比较操作而直接取得所查记录。

常用算法

  • 检索:检索就是在数据结构里查找满足一定条件的节点。一般是给定一个某字段的值,找具有该字段值的节点。
  • 插入:往数据结构中增加新的节点。
  • 删除:把指定的结点从数据结构中去掉。
  • 更新:改变指定节点的一个或多个字段的值。
  • 排序:把节点按某种指定的顺序重新排列。例如递增或递减。

插入排序

基本思想:插入排序的工作方式像许多人排序一手扑克牌。开始时,我们的左手为空并且桌子上的牌面向下。然后,我们每次从桌子上拿走一张牌并将它插入左手中正确的位置。为了找到一张牌的正确位置,我们从右到左将它与已在手中的每张牌进行比较。拿在左手上的牌总是排序好的,原来这些牌是桌子上牌堆中顶部的牌 [1]  。

插入排序是指在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序 [3]  。

package main

import (
    "fmt"
    "math/rand"
    "time"
)
/*
插入排序:从小到大排序
 */

func insertSort(arr []int) []int {
    len := len(arr)//数组长度

    for i := 1; i < len; i++ {
        deal := arr[i] //待排序的数
        j := i - 1 //待排序左边第一个数的位置

        // 如果第一次比较,比左边的已排好序的第个数小,那么进入处理
        if deal < arr[j] {
            //一直往左边找,比待排序大的数都往后挪,腾空位给待排序插入
            for ; j >= 0 && deal < arr[j]; j-- {
                arr[j+1] = arr[j] //某数后移,给待排序留空位
            }
            arr[j+1] = deal //将最后一个空位填充
        }
    }
    return arr
}

func main() {
    //构造一个数组
    arr := []int{0,0,0,0,0,0,0,0}
    //随机数 rand.Seed(种子)
    rand.Seed(time.Now().UnixNano())

    for i := 0;i < len(arr);i++ {
        arr[i] = rand.Intn(20)
    }
    fmt.Printf("%v",insertSort(arr))
}
快速排序

快速排序算法通过多次比较和交换来实现排序,其排序流程如下: 

(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。

(2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于分界值,而右边部分中各元素都大于或等于分界值。 

(3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。

(4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

// 第一种写法
func quickSort(values []int, left, right int) {
    temp := values[left]
    p := left
    i, j := left, right

    for i <= j {
        for j >= p && values[j] >= temp {
            j--
        }
        if j >= p {
            values[p] = values[j]
            p = j
        }

        for i <= p && values[i] <= temp {
            i++
        }
        if i <= p {
            values[p] = values[i]
            p = i
        }
    }
    values[p] = temp
    if p-left > 1 {
        quickSort(values, left, p-1)
    }
    if right-p > 1 {
        quickSort(values, p+1, right)
    }
}

func QuickSort(values []int) {
    if len(values) <= 1 {
        return
    }
    quickSort(values, 0, len(values)-1)
}

// 第二种写法
func Quick2Sort(values []int) {
    if len(values) <= 1 {
        return
    }
    mid, i := values[0], 1
    head, tail := 0, len(values)-1
    for head < tail {
        fmt.Println(values)
        if values[i] > mid {
            values[i], values[tail] = values[tail], values[i]
            tail--
        } else {
            values[i], values[head] = values[head], values[i]
            head++
            i++
        }
    }
    values[head] = mid
    Quick2Sort(values[:head])
    Quick2Sort(values[head+1:])
}

// 第三种写法
func Quick3Sort(a []int,left int, right int)  {

    if left >= right {
        return
    }

    explodeIndex := left

    for i := left + 1; i <= right ; i++ {

        if a[left] >= a[i]{

            //分割位定位++
            explodeIndex ++;
            a[i],a[explodeIndex] = a[explodeIndex],a[i]


        }

    }

    //起始位和分割位
    a[left], a[explodeIndex] = a[explodeIndex],a[left]

    Quick3Sort(a,left,explodeIndex - 1)
    Quick3Sort(a,explodeIndex + 1,right)

}

堆排序

在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:

  • 最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
  • 创建最大堆(Build Max Heap):将堆中的所有数据重新排序
  • 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算 
package main

import "fmt"

// 堆排序的步骤分为两步:1、构建大(小)根堆 2、调整根堆
// 1、构建堆,把最值元素放到父节点,从最后一个非叶子节点开始调整,直到i=0(非叶子节点=0...(n/2-1))
// 2、把堆顶和未调整堆的最后一个元素交换,然后i--继续执行1和2步骤
// 由于和选择排序一样是交换排序,所以堆排序也是不稳定排序
func main() {
    defer fmt.Println("heap sort complete")
    cha1n := make(chan []int)
    var array = []int{10, 2, 7, 9, 4, 11}
    go HeapSort(cha1n, aray)
    fmt.Println(<-cha1n)
}

func HeapSort(cha1n chan<-[]int, nums []int) {
    // 1、构建堆(这里用大顶堆构建升序)
    // 2、调整堆,把堆顶元素和第i-1个元素交换,这样0....i-2就又成为一个堆,继续对这个堆进行构建,调整
    Hepify(nums, len(nums))     // 先构建n个元素的大顶堆
    for i := len(nums) - 1; i >= 0; i-- {
        nums[i],nums[0] = nums[0],nums[i]        // 调整堆顶元素,把堆顶元素和最后一个元素交换
        Hepify(nums, i) 
    }

    cha1n<-nums
}

// 构建堆,一般从最后一个非叶子节点开始构建,即从下往上调整,从下往上能让最大(小)值元素转移到堆顶
func Hepify(nums []int, unsortCapacity int) {
    for i := (unsortCapacity / 2) - 1; i >= 0; i-- {        // 非叶子节点的i范围从0...(n/2-1)个
        // 调整左子树
        leftIndex  := 2*i + 1
        if leftIndex < unsortCapacity && nums[i] < nums[leftIndex] {
            nums[i],nums[leftIndex] = nums[leftIndex], nums[i]        // 左孩子值大于父节点,交换
        } 
        // 调整右子树
        rightIndex := 2*i + 2
        if rightIndex < unsortCapacity && nums[i] < nums[rightIndex] {
            nums[i],nums[rightIndex] = nums[rightIndex], nums[i]    // 右孩子值大于父节点,交换    
        }
    }
}

运行结果为:[11 10 9 7 4 2] heap sort complete [Finished in 0.7s]