【数据结构与算法】八种常见算法

2.1. 排序

2.1.1. 选择排序

  1. SelectionSort 函数:这个函数使用选择排序算法对整数数组进行排序。它通过遍历数组,每次找到最小的元素并将其放到正确的位置。
  2. 循环:
    外循环:从数组的第一个元素开始,逐渐处理到倒数第二个元素。
    内循环:在未排序的部分中找到最小的元素。
  3. 找到最小元素:通过内循环比较每个元素,更新最小元素的索引。
  4. 交换元素:将最小元素与当前位置的元素交换,从而将数组逐渐排序。
  5. main 函数:在这里进行了测试,创建了一个示例数组,然后调用 SelectionSort 函数对其进行排序,并输出排序前后数组的内容。
package main

import "fmt"

// SelectionSort 使用选择排序算法对整数数组进行排序
func SelectionSort(arr []int) {
    // 遍历未排序部分的索引范围
    for i := 0; i < len(arr)-1; i++ {
        // 假设当前索引位置的元素为最小值的索引
        minIndex := i

        // 在未排序部分找到最小值的索引
        for j := i + 1; j < len(arr); j++ {
            if arr[j] < arr[minIndex] {
                minIndex = j
            }
        }

        // 将找到的最小值与当前位置的元素交换
        arr[i], arr[minIndex] = arr[minIndex], arr[i]
    }
}

func main() {
    // 测试选择排序算法
    arr := []int{64, 25, 12, 22, 11}
    fmt.Println("原始数组:", arr)
    SelectionSort(arr)
    fmt.Println("排序后的数组:", arr)
}

2.1.2. 冒泡排序

  1. BubbleSort 函数使用冒泡排序算法对传入的整数数组 arr 进行排序。
  2. 在外层循环中,通过 for 循环遍历数组,每次循环将找到的最大值放在末尾,因此循环次数为 n-1 次,其中 n 是数组的长度。
  3. 在内层循环中,通过 for 循环控制比较和交换元素的过程,每次循环会比较相邻的两个元素,并将较大的元素交换到后面。
  4. 每次内层循环结束后,数组的最大值都会冒泡到未排序部分的末尾,因此每次遍历都会有一个元素被放置在正确的位置上。
  5. main 函数中,通过传入测试数组并调用 BubbleSort 函数来测试冒泡排序算法的正确性。
package main

import "fmt"

// BubbleSort 使用冒泡排序算法对整数数组进行排序
func BubbleSort(arr []int) {
    // 数组长度
    n := len(arr)

    // 外层循环控制遍历次数
    for i := 0; i < n-1; i++ {
        // 内层循环控制比较并交换元素的过程
        // 每次遍历会将最大的元素冒泡到未排序部分的末尾
        for j := 0; j < n-i-1; j++ {
            // 比较相邻的两个元素,如果顺序不对则交换它们
            if arr[j] > arr[j+1] {
                arr[j], arr[j+1] = arr[j+1], arr[j] // 交换 arr[j] 和 arr[j+1]
            }
        }
    }
}

func main() {
    // 测试冒泡排序算法
    arr := []int{64, 25, 12, 22, 11}
    fmt.Println("原始数组:", arr)
    BubbleSort(arr)
    fmt.Println("排序后的数组:", arr)
}

2.1.3. 插入排序(时间复杂度O(N2)、空间复杂度O(1))

package main

import "fmt"

// InsertionSort 对传入的整数切片进行插入排序
func InsertionSort(arr []int) {
    n := len(arr)
    if n <= 1 {
        return // 如果数组长度小于等于1,则无需排序
    }

    for i := 1; i < n; i++ {
        key := arr[i] // 当前待插入的值
        j := i - 1    // 已排序部分的最后一个元素索引

        // 将大于待插入值的元素依次后移,为待插入值腾出位置
        for j >= 0 && arr[j] > key {
            arr[j+1] = arr[j]
            j--
        }

        // 将待插入值插入到正确的位置
        arr[j+1] = key
    }
}

func main() {
    arr := []int{12, 11, 13, 5, 6}
    fmt.Println("未排序前:", arr)
    InsertionSort(arr)
    fmt.Println("排序后:", arr)
}

2.1.4. 归并排序

2.1.4.1. 图解

2.1.4.2. 代码实现

package main

import "fmt"

// MergeSort 对传入的整数切片进行归并排序
func MergeSort(arr []int) {
	n := len(arr)
	if n <= 1 {
		return // 如果数组长度小于等于1,则无需排序
	}

	// 递归地将数组分成两半
	mid := n / 2
	left := make([]int, mid)
	right := make([]int, n-mid)
	copy(left, arr[:mid])
	copy(right, arr[mid:])

	// 递归排序左右两半部分
	MergeSort(left)
	MergeSort(right)
// 合并已排序的左右两部分
	merge(arr, left, right)
}

// merge 函数用于将已排序的左右两部分合并成一个有序数组
func merge(arr, left, right []int) {
	i, j, k := 0, 0, 0
	nLeft, nRight := len(left), len(right)

	// 将左右两部分按顺序合并成一个有序数组
	for i < nLeft && j < nRight {
		if left[i] <= right[j] {
			arr[k] = left[i]
			i++
		} else {
			arr[k] = right[j]
			j++
		}
		k++
	}

	// 处理剩余的元素
	for i < nLeft {
		arr[k] = left[i]
		i++
		k++
	}
	for j < nRight {
		arr[k] = right[j]
		j++
		k++
	}
}

func main() {
	arr := []int{12, 11, 13, 5, 6, 7}
	fmt.Println("未排序前:", arr)
	MergeSort(arr)
	fmt.Println("排序后:", arr)
}

2.1.5. 堆排序

2.1.5.1. 代码实现

堆排序是一种基于堆数据结构的排序算法,它利用了堆的特性来实现排序。堆是一种特殊的树形数据结构,具有以下特点:

  1. 堆是一个完全二叉树,即除了最后一层,其它层的节点都是满的,且最后一层的节点都尽量靠左排列。
  2. 在最大堆中,父节点的值始终大于或等于其子节点的值。
  3. 在最小堆中,父节点的值始终小于或等于其子节点的值。

堆排序的基本思想是:

  1. 将待排序的序列构建成一个最大堆(或最小堆)。
  2. 依次将堆顶元素(最大值或最小值)与堆中最后一个元素交换,并将堆的大小减一,然后对堆进行调整,使得堆重新满足堆的性质。
  3. 重复步骤 2,直到堆的大小为 1,即所有元素都已经有序。

堆排序的实现可以分为两个主要步骤:

  1. 建堆(Heapify):将待排序的序列构建成一个堆,通常采用自底向上的方式从最后一个非叶子节点开始进行调整,直到根节点。
  2. 排序:重复执行以下操作,直到堆的大小为 1:
    • 将堆顶元素与堆中最后一个元素交换。
    • 将堆的大小减一。
    • 对交换后的堆进行调整,使得它重新满足堆的性质。

堆排序的时间复杂度为 O(nlogn),其中 n 为序列的长度。堆排序是一种原地排序算法,不需要额外的空间开销,但它不是稳定的排序算法。

package main

import "fmt"

// 堆排序函数
func HeapSort(arr []int) {
	// 构建最大堆
	for i := 1; i < len(arr); i++ {
		heapInsert(arr, i)
	}
	// 依次将堆顶元素与最后一个元素交换,并调整堆
	for i := len(arr) - 1; i > 0; i-- {
		arr[0], arr[i] = arr[i], arr[0]
		heapify(arr, 0, i)
	}
}

// 向上调整,维护最大堆性质
func heapInsert(arr []int, index int) {
	for arr[index] > arr[(index-1)/2] { // 当前节点大于其父节点
		arr[index], arr[(index-1)/2] = arr[(index-1)/2], arr[index]
		index = (index - 1) / 2
	}
}

// 向下调整,维护最大堆性质
func heapify(arr []int, index, size int) {
	left := 2*index + 1
	for left < size {
		largest := left
		right := left + 1
		if right < size && arr[right] > arr[left] { // 右孩子存在且大于左孩子
			largest = right
		}
		if arr[index] >= arr[largest] { // 当前节点大于等于孩子节点中的最大值
			break
		}
		arr[index], arr[largest] = arr[largest], arr[index]
		index = largest
		left = 2*index + 1
	}
}

func main() {
	arr := []int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5}
	HeapSort(arr)
	fmt.Println(arr) // 输出:[1 1 2 3 3 4 5 5 5 6 9]
}

2.1.6. shell排序

2.1.6.1. 图解

2.1.6.2. 代码实现

Shell排序是插入排序的一种改进版本,也被称为缩小增量排序。它通过将待排序的数组分成多个子序列,对每个子序列进行插入排序,然后逐步缩小子序列的长度,最终完成排序。Shell排序的时间复杂度取决于增量序列的选择,常见的增量序列包括希尔增量序列(h = h/3+1)和Sedgewick增量序列(1, 5, 19, 41, 109...)等。

这段代码实现了希尔排序算法,通过不断缩小增量 h 的方式,对子序列进行插入排序,最终完成整个数组的排序。在主函数中,我们定义了一个整数切片 arr,对其调用 shellSort 函数进行排序,并输出排序后的结果。

希尔排序的时间复杂度约为 O(n^1.3),比插入排序的时间复杂度 O(n^2) 要好,尤其是对大规模数据进行排序时效果明显。

package main

import "fmt"

// shellSort 使用希尔增量序列对传入的整数切片进行排序
func shellSort(arr []int) {
	n := len(arr)
	// 计算初始增量,采用希尔增量序列 h = h/3 + 1
	h := 1
	for h < n/3 {
		h = h*3 + 1
	}

	// 对每个增量进行排序
	for h >= 1 {
		// 对每个子序列进行插入排序
		for i := h; i < n; i++ {
			// 将 arr[i] 插入到 arr[i-h], arr[i-2*h], arr[i-3*h]... 中合适的位置
			for j := i; j >= h && arr[j] < arr[j-h]; j -= h {
				arr[j], arr[j-h] = arr[j-h], arr[j]
			}
		}
		// 缩小增量
		h /= 3
	}
}

func main() {
	arr := []int{5, 2, 4, 6, 1, 3}
	fmt.Println("原数组:", arr)
	shellSort(arr)
	fmt.Println("排序后数组:", arr)
}

2.1.7. 快速排序

package main

import "fmt"

// 快速排序函数
func quickSort(arr []int, low, high int) {
	if low < high {
		// 划分
		pivot := partition(arr, low, high)
		// 递归排序左半部分
		quickSort(arr, low, pivot-1)
		// 递归排序右半部分
		quickSort(arr, pivot+1, high)
	}
}

// 划分函数
func partition(arr []int, low, high int) int {
	// 选取最后一个元素作为基准值
	pivot := arr[high]
	// i 指向小于基准值的元素的位置
	i := low - 1

	// 遍历数组,将小于基准值的元素交换到左边
	for j := low; j <= high-1; j++ {
		if arr[j] < pivot {
			i++
			arr[i], arr[j] = arr[j], arr[i]
		}
	}

	// 将基准值交换到正确的位置
	arr[i+1], arr[high] = arr[high], arr[i+1]
	return i + 1
}

func main() {
	arr := []int{9, 7, 5, 11, 12, 2, 14, 3, 10, 6}
	fmt.Println("Unsorted array:", arr)

	// 调用快速排序函数
	quickSort(arr, 0, len(arr)-1)

	fmt.Println("Sorted array:", arr)
}

2.1.8. 桶排序

2.1.8.1. 计数排序
2.1.8.1.1. 图解

2.1.8.1.2. 代码实现

计数排序是一种非比较排序算法,适用于一定范围内的整数排序。它的基本思想是统计每个整数在序列中出现的次数,然后根据统计信息将这些整数按顺序输出到结果数组中。

以下是计数排序的基本步骤:

  1. 找出待排序数组中的最大值 max 和最小值 min,并根据它们确定计数数组的大小。
  2. 初始化一个计数数组 count,长度为 max - min + 1,并将所有元素初始化为0。
  3. 遍历待排序数组,统计每个元素出现的次数,将计数结果存储在计数数组中。
  4. 根据计数数组中的统计信息,将待排序数组中的元素按顺序放入结果数组中。
  5. 将结果数组中的元素复制回原始数组,完成排序。
package main

import "fmt"

func countSort(arr []int) {
	if len(arr) == 0 {
		return
	}

	// 找出数组中的最大值和最小值
	min, max := arr[0], arr[0]
	for _, num := range arr {
		if num < min {
			min = num
		}
		if num > max {
			max = num
		}
	}

	// 初始化计数数组
	count := make([]int, max-min+1)

	// 统计每个元素出现的次数
	for _, num := range arr {
		count[num-min]++
	}

	// 根据计数数组构造排序后的结果
	index := 0
	for i := min; i <= max; i++ {
		for count[i-min] > 0 {
			arr[index] = i
			index++
			count[i-min]--
		}
	}
}

func main() {
	arr := []int{4, 2, 2, 8, 3, 3, 1}
	fmt.Println("原数组:", arr)
	countSort(arr)
	fmt.Println("排序后数组:", arr)
}
  • 29
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值