数据结构-排序(Golang)
排序算法我认为是算法里重要等级很高的内容了,排序在生产生活中应用的非常多,几乎到处都有排序问题,今天我就来把常见的排序算法总结一下。
01
排序基本概念
1
排序的定义
排序就是重新排列表中的元素,使表中的元素满足按关键字递增或递减的过程。
算法的稳定性:如果待排序表中有两个元素Ri、Rj,其对应的关键字 keyi =keyj,且在排序前Ri在Rj前面,如果使用某一排序算法排序后,Ri仍然在Rj前面,则称这个排序算法是稳定的,否则称该算法是不稳定的。
在排序过程中,按照数据元素是否完全在内存中,可将排序算法分为两类:内部排序是指在排序期间元素全部存放在内存中的排序;外部排序是指在排序期间元素无法全部同时存放在内存中,必须在排序的过程中根据要求不断地在内、外存之间移动的排序。
02
插入排序
插入排序其基本思想是每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。由插入排序的思想可以引申出三个重要的排序算法:直接插入排序、折半插入排序和希尔排序。
1
直接插入排序
假设在排序过程中,待排序L[1....n]在某次排序过程中的某一时刻状态如下
为了是实现将元素L(i)插入到已有序的子序列L[1....i-1]中,我们需要执行以下操作。
查找出L(i)在表L[1....i-1]中的插入位置k。
将L[k....i-1]中所有元素全部后移一个位置。
将L(i)复制到L(k)。
【算法实现】
package main
import (
"fmt"
//"sort"
)
//InsertSort method
//直接插入排序
func InsertSort(a []int) {
var i, j int
for i = 1; i < len(a); i++ { //第一次是认为第一个元素已经是有序的,故从第二个位置开始
temp := a[i] //将当前元素设为哨兵,临时变量temp,可以考虑直接赋值给a[0],代码改进的地方
for j = i - 1; j >= 0 && temp < a[j]; j-- { //比较元素大小,从后往前比较
a[j+1] = a[j] //向后挪位置
}
a[j+1] = temp //将哨兵元素赋值到插入位置
}
fmt.Println(a)
}
func main() {
a := []int{1, 2, 6, 4, 78, 50}
fmt.Println(a[1:5])
InsertSort(a)
InsertSort1(a)
fmt.Printf("%T\n", a)
fmt.Println(a[1:5])
}
【算法分析】
【算法过程示例】
2
折半插入算法
在上面原始插入算法中,我们总是边比较边移动元素,我们因此引入折半插入算法,将比较和移动操作分离出来,即先折半查找元素的待插入位置,然后再统一地移动待插入位置之后的所有元素。
【算法实现】
//InsertSort1 method
//折半插入排序
func InsertSort1(a []int) {
var i, j, low, high, mid int
for i = 2; i < len(a); i++ {
temp := a[i]
low, high = 1, i-1
if low <= high {
mid = (high + low) / 2
if a[mid] > temp {
high = mid - 1
} else {
low = mid + 1
}
for j = i - 1; j >= high+1; j-- {
a[j+1] = a[j]
}
a[high+1] = temp
}
}
fmt.Println(a)
}
3
希尔排序
直接插入排序算法适用于基本有序的排序表和数据量不大的排序表,基于这点,D.L.Shell提出了希尔排序,又称缩小增量排序。
希尔排序序的基本思想是:先将待排序表分割成若干形如L[i,i+d, i+2d,...,i+kd]的“特殊”子表,即把相隔某个“增量”的记录组成一个子表,对各个子表分别进行直接插入排序,当整个表中的元素己呈“基本有序”时,再对全体记录进行一次直接插入排序。
【算法实现】
//ShellSort method
//希尔排序
func ShellSort(a []int) {
n := len(a)
for dk := n / 2; dk >= 1; dk = dk / 2 {
for i := dk + 1; i < n; i++ {
temp := a[i]
j:=i-dk
for ; j >= 0 && temp < a[j]; j -= dk {
a[j+dk] = a[j]
}
a[j+dk] = temp
}
}
fmt.Println(a)
}
//代码更清楚的版本
func shellSort(arr []int) {
len := len(arr)
// 增量
inc := 2
// 步长
step := len / inc
for ; step > 0; step /= inc {
for r := step; r < len; r ++ {
// 最小下标为0
l := r - step
tmp := arr[r]
for ; l >= 0 && tmp < arr[l]; l -= step {
arr[l+step] = arr[l]
}
arr[l+step] = tmp
}
}
fmt.Println(arr)
}
【算法过程示例】
第一趟取增量d1= 5,将该序列分成5个子序列,即图中第2行至第6行,分别对各子序列进行直接插入排序,结果如第7行所示;第二趟取增量d2= 3,分别对3个子序列进行直接插入排序,结果如第11行所示;最后对整个序列进行一趟直接插入排序。
【算法分析】
03
交换排序
交换,就是根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置。基于交换的排序算法有很多,我们主要总结下冒泡排序和快速排序。
1
冒泡排序
冒泡排序的基本思想是:从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i-1]>A[i]),则交换它们,直到序列比较完。我们称它为第一趟冒泡,结果是将最小的元素交换到待排序列的第一个位置(或将最大的元素交换到待排序列的最后一个位置),关键字最小的元素如气泡一般逐渐往上“漂浮”直至“水面”(或关键字最大的元素如石头一般下沉至水底)。下一趟冒泡时,前一趟确定的最小元素不再参与比较,每趟冒泡的结果是把序列中的最小元素(或最大元素)放到了序列的最终位置......这样最多做n-1趟冒泡就能把所有元素排好序。
【算法实现】
//BubbleSort method
//冒泡排序
func BubbleSort(a []int) {
n := len(a)
for i := 0; i < n-1; i++ { //总共比较i趟
flag1 := false
for j := n - 1; j > i; j-- { //for j:=1; j<=n-i; j++ 第i趟需要比较n-i次
if a[j-1] > a[j] {
//Swap(&a[j-1], &a[j])
a[j-1], a[j] = a[j], a[j-1]
}
flag1 = true
}
if flag1 == false {
break
}
}
fmt.Println(a)
}
//Swap method
//交换值,注意指针传递,才会真正改变实参的值
func Swap(a, b *int) {
temp := *a
*a = *b
*b = temp
}
// func Swap1(a, b int) {
// temp := a
// a = b
// b = temp
// }
//冒泡排序简单的一个递归算法
func bubbleSort(arr []int, len int) {
if len == 1 {
return
}
for i := 0; i < len-1; i++ {
if arr[i] > arr[i+1] {
arr[i], arr[i+1] = arr[i+1], arr[i]
}
}
bubbleSort(arr, len-1)
}
【算法过程】
【算法分析】
2
快速排序
快速排序的基本思想是基于分治法的:在待排序表L[1...n]中任取一个元素pivot作为枢轴(或基准,通常取首元素),通过一趟排序,将待排序表划分为独立的两部分L[1...k-1]和L[k+1...n],使得L[1...k-1]中的所有元素小于pivot,L [k+1...n]中的所有元素大于等于pivot,则pivot放在了其最终位置L(k)上,这个过程称为一趟快速排序(或一次划分)。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。
【算法实现】
//
//快速排序递归算法
func qsort(array []int, low, high int) {
if low < high {
m := partition(array, low, high)
// fmt.Println(m)
qsort(array, low, m-1)
qsort(array, m+1, high)
}
}
//划分方法
func partition(array []int, low, high int) int {
key := array[low]
tmpLow := low
tmpHigh := high
for {
//查找小于等于key的元素,该元素的位置一定是tmpLow到high之间,因为array[tmpLow]及左边元素小于等于key,不会越界
for array[tmpHigh] > key {
tmpHigh--
}
//找到大于key的元素,该元素的位置一定是low到tmpHigh+1之间。因为array[tmpHigh+1]必定大于key
for array[tmpLow] <= key && tmpLow < tmpHigh {
tmpLow++
}
if tmpLow >= tmpHigh {
break
}
// swap(array[tmpLow], array[tmpHigh])
array[tmpLow], array[tmpHigh] = array[tmpHigh], array[tmpLow]
fmt.Println(array)
}
array[tmpLow], array[low] = array[low], array[tmpLow]
return tmpLow
}
【算法过程】
【算法分析】
在学习中会有很多对快速排序的改进算法,但我们首先需要掌握最最基础的原理才可以。
04
选择排序
选择排序的基本思想是:每一趟(如第i趟)在后面n-i+1 ( i = 1 , 2,...., n-1)个待排序元素中选取关键字最小的元素,作为有序子序列的第i个元素,直到第n-1趟做完,待排序元素只剩下1个,就不用再选了。
1
简单选择排序
简单选择排序算法的思想:假设排序表为L[1...n],第1趟排序即从L[i...n]中选择关键字最小的元素与L(i)交换,每一趟排序可以确定一个元素的最终位置,这样经过n-1趟排序就可使得整个排序表有序。
【算法实现】
//简单选择排序
func selectsort(arr []int) {
n := len(arr)
for i := 0; i < n-1; i++ { //一共进行n-1趟
min := i //记录最小元素位置
for j := i + 1; j < n; j++ { //在arr[i...n-1]中选择最小的元素
if arr[j] < arr[min] {
min = j //更新最小的元素位置
}
}
if min != i {
arr[i], arr[min] = arr[min], arr[i] //与第i个位置交换
}
}
fmt.Println(arr)
}
【算法分析】
2
堆排序
堆排序具体可分为大根堆和小根堆。
【算法思想】
堆排序的思路很简单:首先将存放在L[1...n]中的n个元素建成初始堆,由于堆本身的特点(以大顶堆为例),堆顶元素就是最大值。输出堆顶元素后,通常将堆底元素送入堆顶,此时根结点已不满足大顶堆的性质,堆被破坏,将堆顶元素向下调整使其继续保持大顶堆的性质,再输出堆顶元素。如此重复,直到堆中仅剩一个元素为止。可见堆排序需要解决两个问题:①如何将无序序列构造成初始堆?②输出堆顶元素后,如何将剩余元素调整成新的堆?
咳咳,针对堆排序后续的细节问题,我觉得还是大家去看王卓老师的讲解视频,到底该怎么将被破坏的堆调整到位,文字书写量太大了,堆排序问题考研是经常会考的,一定要重视。
【算法分析】
05
归并排序和基数排序
1
归并排序
归并排序与上述基于交换、选择等排序的思想不一样,“归并”的含义是将两个或两个以上的有序表组合成一个新的有序表。假定待排序表含有n个记录,则可将其视为n个有序的子表,每个子表的长度为1,然后两两归并,得到
个长度为2或1的有序表;继续两两归并......如此重复,直到合并成一个长度为n的有序表为止,这种排序方法称为2路归并排序。
【算法实现】
func mergeSort(arr []int, start, end int) {
if start >= end {
return
}
mid:=(start + end) / 2 //从中间划分两个子序列
mergeSort(arr, start, mid) //对左侧子序列进行递归排序
mergeSort(arr, mid+1, end) //对右侧子序列进行递归排序
merge(arr, start, mid, end) //归并
}
//merge()的功能是将前后相邻的两个有序表归并为一个有序表。
func merge(arr []int, start, mid, end int) {
var tmparr = []int{}
var s1, s2 = start, mid+1
for s1<= mid && s2<= end{
if arr[s1] > arr[s2] {
tmparr = append(tmparr, arr[s2])
s2++
} else {
tmparr = append(tmparr, arr[s1])
s1++
}
}
if s1<=mid {
tmparr = append(tmparr, arr[s1: mid+1]...)
}
if s2<=end {
tmparr = append(tmparr, arr[s2: end+1]...)
}
for pos,item:=range tmparr{
arr[start + pos] = item
}
}
var a=[]int{3,4,0,1,5,6,7,8}
func main() {
mergeSort(a, 0, len(a) - 1)
fmt.Println(a)
}
【算法过程】
【算法分析】
2
基数排序
基数排序我就偷懒不写啦,因为平时我用的少,大家要自己去看看书做做题嗷
06
各种排序算法比较
各种排序算法的比较大家记住这张表就可以了
排序算法这部分就粗略的总结到这里,这部分考研的部分还是很多的,希望大家要反复练手,弄明白每个算法的实现过程,代码实现有困难的同学需要多写一些,没事就想想核心算法是怎么回事,熟能生巧嘛。
另外,我改写的所有Go语言代码都会push到我的github仓库里,大家可以来看看
数据结构还有一张查找还没有总结,我会努力学会了再和大家分享的!