算法学习之内部排序

PS:集百家之长,汇小柒心得。(这杯我先干为敬,想说的都在代码里面了)

一、交换类排序

冒泡排序

/**
 * 冒泡排序(起泡排序)
 * 算法思想:最简单的排序算法,学不会立即推:放弃编程
 * 稳定性:稳定排序
 * 时间复杂度:O(n^2)---最坏情况:n(n-1)/2;最好情况:O(n)
 * 空间复杂度:O(1)
 * 适用场景:
 */
func BubbleSort(a []int) {
	// 判空检查
	if len(a) <= 0 {
		fmt.Println("待排序列为空,请重新输入!!!")
	}

	// 起泡算法结束的条件:在一趟排序过程中没有发生关键字的交换
	var flag bool // 标记位:该躺排序中是否发生了关键字交换
	for i := 0; i < len(a); i++ {
		flag = false
		for j := 0; j < len(a)-i-1; j++ {
			if a[j] > a[j+1] {
				tmp := a[j]
				a[j] = a[j+1]
				a[j+1] = tmp
				flag = true
			}
		}
		if !flag {
			break
		}
	}

	fmt.Println("冒泡排序后的结果为:")
	algorithms.PrintArray(a)
}

// 冒泡排序进阶:双向冒泡排序
func BubbleSortDoubleDirection(arr []int) {
	var left int = 0
	var right int = len(arr) - 1
	var flag bool = true

	for flag {
		flag = false
		for i := left; i < right; i++ {
			if arr[i] > arr[i+1] {
				algorithms.Swap(arr, i, i+1)
				flag = true
			}
		}
		right--

		for j := right; j > left; j-- {
			if arr[j] < arr[j-1] {
				algorithms.Swap(arr, j, j-1)
				flag = true
			}
		}
		left++
	}
}

快速排序

/**
 * 快速排序
 * 算法思想:分治
 * 稳定性:不稳定排序
 * 时间复杂度:O(n*lgn)---最坏情况:O(n^2);最好情况:O(n*lgn)
 * 空间复杂度:O(lgn)
 * 适用场景:待排序列越接近无序,效率越高。快速排序的排序趟数和初始序列有关。与同级别时间复杂度都为O(n*lgn)的排序算法相比,
 * 			该算法的基本操作的最高次项的系数最小,效率最高,故而称为快速排序。
 */
func QuickSort(a []int, low int, high int) {
	// 判空检查
	if len(a) <= 0 {
		fmt.Println("待排序列为空,请重新输入!!!")
	}

	// 以低位为枢轴进行快速排序(从a[low]到a[high]的关键字进行排序)
	i := low  // 低位
	j := high //高位
	if low < high {
		tmp := a[low]
		// 下面这个循环完成一趟排序,即将数组中小于tmp的关键字放在左边,大于tmp的关键字放在右边
		for i < j {
			// 从右往左扫描,找到一个小于tmp的关键字
			for i < j && tmp <= a[j] {
				j--
			}
			if i < j {
				a[i] = a[j] // 放在tmp左边
				i++         // i右移一位
			}

			// 从左往右扫描,找到一个大于tmp的关键字
			for i < j && tmp > a[i] {
				i++
			}
			if i < j {
				a[j] = a[i] // 放在tmp的右边
				j--         // j左移一位
			}
		}
		// 循环结束,i==j,该位置为当前枢轴的最终位置,将tmp放在该位置
		a[i] = tmp
		// 打印划分结果
		fmt.Println("快速排序划分后为:")
		algorithms.PrintArray(a)
		// 递归地对tmp左边的关键字进行排序
		QuickSort(a, low, i-1)
		// 递归地对tmp右边的关键字进行排序
		QuickSort(a, i+1, high)
	}
}

二、插入类排序

直接插入排序

/**
 * 直接插入排序
 * 算法思想:每趟将一个待排序的关键字按照其值的大小插入到已经排好的部分有序序列的适当位置上,直到所有待排关键字都被插入到有序序列中为止。
 * 稳定性:稳定排序
 * 时间复杂度:O(n^2)---最坏情况:n(n-1)/2;最好情况:O(n)
 * 空间复杂度:O(1)
 * 适用场景:适用于序列基本有序的情况
 */
func DirectInsertSort(a []int) {
	// 判空检查
	if len(a) <= 0 {
		fmt.Println("待排序列为空,请重新输入!!!")
	}

	// 1.因为第一个数天然有序,所以从第二位才开始进行插入排序
	for i := 1; i < len(a); i++ {
		// 2.从本算法思想可以看出,直接插入排序的核心思想是找到待排关键字在本趟排序中的恰当位置,所以无可避免的是要对原始序列
		// 元素进行移位操作。为统一化,也为了方便理解,以下代码是数组元素移位操作的基本模板代码
		// 2.1 首先记录待排关键字,因为在移位操作中,该关键字可能会被覆盖
		temp := a[i]
		// 2.2 一步步向前比较,找到合适的位置,直到数组到头
		j := i - 1
		for j >= 0 && a[j] > temp {
			// 如果当前比较位置的关键字比待排关键字大,则后移当前位置的关键字,下标指向当前位置的前一位
			a[j+1] = a[j]
			j--
		}
		// 3.经过上面的移位操作,j指向了待排关键字需要插入位置的前一位,插入即可
		a[j+1] = temp
	}

	// 打印结果
	fmt.Println("直接插入排序后的结果为:")
	algorithms.PrintArray(a)
}

// @desc 直接插入排序(单链表)
// @param *ListNode 单链表头结点的地址
func DirectInsertSortOfLinkList(head *ListNode) {
	var cur, tmp, node, pre *ListNode
	// 1.单链表不为空
	if head.Next != nil {
		// 2.默认第一个结点有序,cur指向单链表第一个结点的后继
		cur = head.Next.Next
		// 3.单链表断链
		head.Next.Next = nil
		// 4.第一个结点有直接后继时,才真正进行直接插入排序
		for cur != nil {
			// 4.1 pre指向头结点
			pre = head
			// 4.2 node指向第一个结点
			node = pre.Next
			for node != nil && node.Val < cur.Val {
				// 4.3 在有序表中找到一个结点p,使其val值刚好大于cur.val
				pre = node
				node = node.Next
			}
			// 4.4 将cur插入到已找到的位置
			tmp = cur.Next
			pre.Next = cur
			cur.Next = node
			cur = tmp
		}
	}
}

折半插入排序

/**
 * 折半插入排序
 * 算法思想:与直接插入排序类似,只是在查找插入位置时使用二分查找以提升查找速率(并不影响算法的时间复杂度)
 * 稳定性:稳定排序
 * 时间复杂度:O(n^2)---最坏情况:O(n^2);最好情况:O(n*lgn)
 * 空间复杂度:O(1)
 * 适用场景:适用于关键字树较多的情况
 */
func HalfInsertSort(a []int) {
	// 判空检查
	if len(a) <= 0 {
		fmt.Println("待排序列为空,请重新输入!!!")
	}

	for i := 1; i < len(a); i++ {
		j := -1 // 标记位,记录一个比目标值大且最接近目标值的数的数组下标
		mid := 0
		head := 0
		tail := i - 1

		/* 以下这段代码还是挺难理解的,书上也没有折半查找的代码,现在很好奇这段代码是在哪抄的还是自己想出来的,哈哈哈!!!(2021/10/16 14:54)*/
		/* 这有啥难理解的,不很简单吗???但是不得不说这个标记位用得还是很妙的(2021/11/25 22:49 )*/
		// 以下代码用于查找一个最小的且比目标值大的数
		for head <= tail { // 当head==tail时,也要进行判断
			mid = (head + tail) / 2
			// 该循环是找一个比目标值大且最近的数,故只有在目标值小于某一个数的时候做标记
			if a[i] < a[mid] {
				j = mid
				tail = mid - 1
			} else {
				head = mid + 1
			}
		}

		// 以下代码用于移动元素操作
		if j >= 0 {
			tmp := a[i]
			k := i
			for k > j {
				a[k] = a[k-1]
				k--
			}
			a[k] = tmp
		}
	}

	// 打印结果
	fmt.Println("折半插入排序后的结果为:")
	algorithms.PrintArray(a)
}

希尔排序

/**
 * 希尔排序(缩小增量排序)
 * 算法思想:使序列变得越来越有序,从而让直接插入排序效率更高
 * 稳定性:不稳定排序
 * 时间复杂度:与增量选取有关
 * 空间复杂度:O(1)
 * 适用场景:
 */
func ShellSort(a []int) {

}

三、选择类排序

简单选择排序

/**
 * 简单选择排序
 * 算法思想:过于简单,无需赘述
 * 稳定性:不稳定排序
 * 时间复杂度:O(n^2)
 * 空间复杂度:O(1)
 * 适用场景:
 */
func SimpleSelectSort(a []int) {
	// 判空检查
	if len(a) <= 0 {
		fmt.Println("待排序列为空,请重新输入!!!")
	}

	var index int // 最小值的数组下标
	var tmp int   // 临时变量
	for i := 0; i < len(a); i++ {
		// 默认当前值为最小值
		index = i
		// 本算法精髓:从无序序列中挑出一个最小的关键字
		for j := i + 1; j < len(a); j++ {
			if a[index] > a[j] {
				index = j
			}
		}
		tmp = a[i]
		a[i] = a[index]
		a[index] = tmp
	}

	fmt.Println("简单选择排序后的结果为:")
	algorithms.PrintArray(a)
}

堆排序

/**
 * 堆排序
 * 算法思想:利用堆这种数据结构本身的特点来进行排序操作。
 * 稳定性:不稳定排序
 * 时间复杂度:O(n*lgn):建堆的时间复杂度为 O(n),调整堆的时间复杂度为 O(lgn),其中调用了 n-1 次,因此堆排序的时间复杂度为 O(n)+O(n*lgn) ~ O(n*lgn)
 *			堆排序在最坏情况下的时间复杂度也是O(n*lgn),这是它相对于快速排序最大的优点。
 * 空间复杂度:O(1)。堆排序的空间复杂度为O(1),在所有时间复杂度为O(n*lgn)的排序中是最小的,这也是其一大优点。
 * 适用场景:适用的场景是关键字很多的情况,典型的例子是从10 000个关键字中选出前十个最小的,这种情况用堆排序最好。
 *			如果关键字个数较少,则不推荐使用堆排序。
 */
func HeapSort(a []int) {
	// 判空检查
	if len(a) <= 0 {
		fmt.Println("待排序列为空,请重新输入!!!")
	}

	// 建立初始堆,从第一个非叶子结点开始调整
	// 注:1.若数组下标从1开始,则第一个非叶结点为 n/2 取下整;
	//     2.若数组下标从0开始,则第一个非叶结点为 (n-2)/2 取下整。
	for i := len(a)/2 - 1; i >= 0; i-- {
		Shift(a, i, len(a)-1)
	}
	// 进行len(a)-1次循环,完成堆排序
	var temp int // 临时变量
	for i := len(a) - 1; i >= 1; i-- {
		// 换出根结点中的关键字,将其放入最终位置
		temp = a[0]
		a[0] = a[i]
		a[i] = temp
		// 在减少了一个关键字的无序序列中进行调整
		Shift(a, 0, i-1)
	}

	fmt.Println("堆排序后的结果为:")
	algorithms.PrintArray(a)
}

// 在数组 a[low] 到 a[high] 的范围内对在位置 low 上的结点进行调整
func Shift(a []int, low int, high int) {
	// 父结点下标
	i := low
	// a[j] 是 a[i] 的左孩子结点
	//(注意:若数组从下标 0 开始储存数据,则 i 结点对应的左孩子的下标为 2*i+1;若数组从下标 1 开始储存数据,则为 2*i)
	j := 2*i + 1
	temp := a[i] // temp 指向父结点
	for j <= high {
		// 若右孩子较大,则把j指向右孩子
		if j < high && a[j] < a[j+1] {
			j++ // j变为2*i+2
		}
		// 若父结点小于孩子结点的值,说明当前堆不满足大顶堆定义,进行调整
		if temp < a[j] {
			// 将 a[j] 调整到双亲结点的位置上,同时修改 i 和 j 的值,以便继续向下调整
			a[i] = a[j]
			i = j       // 指向进一步向下调整新的父结点
			j = 2*i + 1 // 指向进一步向下调整新父结点的左孩子
		} else {
			break // 调整结束
		}
	}
	// 被调整结点的值放入最终位置
	a[i] = temp
}

四、归并类排序

二路归并排序

/**
 * 二路归并排序
 * 算法思想:将一个前半部分有序和后半部分有序的序列归并为一个完全有序的序列(不开辟新空间)(哈哈哈哈,还是挺绕的哈,晓峰厉害,yyds)
 * 稳定性:稳定排序
 * 时间复杂度:O(n*lgn)
 * 空间复杂度:O(n)
 * 注意:该算法在归并两个有序序列未开辟新的存储空间,是在原有的数组中做了归并(有点难度的),因此时间复杂度比n*lgn要大,
 * 		但空间复杂度为O(1)。书中归并两个有序序列采用了新开辟一个与整个序列同样大小的内存空间来作辅助序列,算法易于实现,
 * 		不会考虑数组插入元素需要后移其他元素的问题,总之,该归并是以时间换空间(其实大可不必---纯属炫技)。
 */
func MergeSort1(a []int, low int, high int) {
	// 递归结束条件
	if low >= high {
		return
	}

	// 防止溢出
	mid := low + (high-low)/2
	// 归并排序前半段
	MergeSort1(a, low, mid)
	// 归并排序后半段
	MergeSort1(a, mid+1, high)
	// 将前半段有序序列和后半段有序序列归并成整体有序序列
	merge1(a, low, mid, high)
	fmt.Println("二路归并排序后的结果为:")
	algorithms.PrintArray(a)

}

// 将两端已经有序的序列合成一条整体有序的序列(以时间换空间)
func merge1(a []int, low int, mid int, high int) {
	// 该归并算法是将后半部分有序序列归并到前半部分有序序列
	var tmp int // 临时变量
	i := mid + 1
	for i <= high && low <= mid {
		if a[low] < a[i] {
			low++
		} else {
			// 将前半部分有序序列整体后移一位
			tmp = a[i]
			j := i
			for j > low {
				a[j] = a[j-1]
				j--
			}
			// 将后半部分插入到合适的位置,再进行下一个数的判断
			a[j] = tmp
			i++
			// 注意:因为前半部分有序序列插入了一个数,所以前半部分有序序列的将要进行下一次比较的下标和尾下标都要相应的向后移一位
			low++
			mid++
		}
	}
}

/*
归并排序,典型的分治算法;分治,典型的递归结构。
分治算法可以分三步⾛:分解 -> 解决 -> 合并
	1. 分解原问题为结构相同的⼦问题。
	2. 分解到某个容易求解的边界之后,进⾏递归求解。
	3. 将⼦问题的解合并成原问题的解。
归并排序,我们就叫这个函数 merge_sort 吧,按照我们上⾯说的,要明确该函数的职责,即对传⼊的⼀个数组排序。OK,那么这个问题能不能分解
呢?当然可以!给⼀个数组排序,不就等于给该数组的两半分别排序,然后合并就完事了。
void merge_sort(⼀个数组) {
	if (可以很容易处理) return;
	merge_sort(左半个数组);
	merge_sort(右半个数组);
	merge(左半个数组, 右半个数组);
}
好了,这个算法也就这样了,完全没有任何难度。记住之前说的,相信函数的能⼒,传给他半个数组,那么这半个数组就已经被排好了。⽽且你会发现
这不就是个⼆叉树遍历模板吗?为什么是后序遍历?因为我们分治算法的套路是:分解 -> 解决(触底) -> 合并(回溯) 啊,先左右分解,再处理合
并,回溯就是在退栈,就相当于后序遍历了。⾄于 merge 函数,参考两个有序链表的合并,简直⼀模⼀样,下⾯直接贴代码吧。
下⾯参考《算法4》的 Java 代码,很漂亮。由此可⻅,不仅算法思想很重要,编码技巧也是挺重要的吧!多思考,多模仿(建议背下来)。
*/
// 不要在 merge 函数里构造新数组了, 因为 merge 函数会被多次调用,影响性能,直接一次性构造一个足够大的数组,简洁,高效
// tmp 为辅助数组(减小算法的空间复杂度)
func MergeSort2(nums, tmp []int, lo, hi int) {
	if lo >= hi {
		return
	}

	mid := lo + (hi-lo)/2
	MergeSort2(nums, tmp, lo, mid)
	MergeSort2(nums, tmp, mid+1, hi)
	merge2(nums, tmp, lo, mid, hi)
}

// 以空间换时间
func merge2(nums, tmp []int, lo, mid, hi int) {
	// 初始化辅助数组,用于记录 nums 中前后两个已经各自有序的序列
	// 因为在归并排序中,原数组(nums)会被修改,故而需要开辟新空间记录原始数据
	for i := lo; i <= hi; i++ {
		tmp[i] = nums[i]
	}

	// 开始归并
	i, j := lo, mid+1
	for k := lo; k <= hi; k++ {
		// 卧槽,这个边界值处理有点牛逼呀---2023/7/27 17:12
		if i > mid {
			// 此时前半部分有序的数组已经归并完毕,仅剩后半部分尚未归并,直接复制即可
			nums[k] = tmp[j]
			j++
		} else if j > hi {
			// 此时后半部分有序的数组已经归并完毕,仅剩前半部分尚未归并,直接复制即可
			nums[k] = tmp[i]
			i++
		} else if tmp[i] <= tmp[j] {
			// 归并:取较小者进入有序序列
			nums[k] = tmp[i]
			i++
		} else {
			// 归并:取较小者进入有序序列
			nums[k] = tmp[j]
			j++
		}
	}

	fmt.Println("二路归并排序后的结果为:")
	algorithms.PrintArray(nums)
}

五、打印切片小函数

func PrintArray(a []int) {
	for i := 0; i < len(a); i++ {
		fmt.Printf("%d\t", a[i])
	}
	fmt.Println()
}```

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值