数据结构与算法06:递归和简单的排序

news/2024/11/29 19:48:58/

目录

【递归】

【排序】

冒泡排序

插入排序

选择排序

【每日一练:K 个一组翻转链表】


【递归】

递归是将一些有规律的重复问题分解为同类的子问题的方法,也就是在函数中自己调用自己。比较经典的递归代码就是 斐波那契数列,实现方式如下:

// 1、1、2、3、5、8、13、21、34
// F(0)=1, F(1)=1, F(n)=F(n-1)+F(n-2)
func getFibonacci(n int) int {if n == 1 || n == 2 {return 1}return getFibonacci(n-1) + getFibonacci(n-2)
}

写递归代码最关键的是写出递推公式,找到终止条件。只要同时满足以下三个条件,就可以用递归来解决:

  •  一个问题的解可以分解为几个子问题的解
  • 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
  • 存在递归终止条件

递归代码要警惕堆栈溢出和重复计算的问题,而且过多的函数调用会耗时较多。由于递归调用一次就会在内存栈中保存一次现场数据,所以递归代码的空间复杂度一般是O(n)。一般能用递归解决的问题也基本都能用循环解决,因此要根据实际情况来选择是否需要用递归的方式来实现。

【排序】

常见的排序算法有下面这些:冒泡排序、插入排序、选择排序、归并排序、快速排序、堆排序、计数排序、基数排序、桶排序。

排序算法时间复杂度是否基于比较
冒泡、插入、选择O(n^2)
快排、归并、堆排序O(nlogn)
桶、计数、基数O(n)
  • 是否基于比较:基于比较的排序算法的执行过程中会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。
  • 原地排序算法:是指空间复杂度是 O(1) 的排序算法。
  • 稳定的排序算法:针对排序算法,不仅要关注执行效率和内存消耗,还要关注稳定性,如果待排序的序列中存在值相等的元素,经过排序之后这些相等元素之间原有的先后顺序应该保持不变。

稳定排序算法可以保持某个key相同的两个对象,在排序之后的前后顺序不变。比如下面这组数据:[{id:1,name:"AA",age:50},{id:2,name:"BB",age:30},{id:3,name:"CC",age:40},{id:4,name:"DD",age:30}],假设需要按照age由小到大排序,由于有两个age=30的数据,如果排序后的结果为 [{id:4,name:"DD",age:30},{id:2,name:"BB",age:30},{id:3,name:"CC",age:40},{id:1,name:"AA",age:50}],就不是一个稳定的排序算法。如果要实现稳定排序,可以对唯一字段(比如id)先排序一次,然后再对age排序。

冒泡排序

冒泡排序只会操作相邻的两个数据,每次冒泡操作都会对相邻的两个元素比较大小,如果不满足条件就让这两个元素互换。如下图所示:

上面第4次和第5次实际并没有交换数据,因此在实现过程中可以判断这种情况直接跳过。下面是一个冒泡排序的代码:

// 冒泡排序,a表示数组,n表示数组大小
func BubbleSort(a []int, n int) {if n <= 1 {return}for i := 0; i < n; i++ {// 提前退出标志flag := falsefor j := 0; j < n-i-1; j++ {if a[j] > a[j+1] {a[j], a[j+1] = a[j+1], a[j]//此次冒泡有数据交换flag = true}}// fmt.Println(a)// 如果没有交换数据,提前跳过if !flag {break}}
}func main() {arr := []int{8, 5, 6, 3, 1, 7}BubbleSort(arr, len(arr))fmt.Println("冒泡排序后:", arr) // [1 3 5 6 7 8]
}

关于冒泡排序的细节分析:

  • 冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1),是一个原地排序算法
  • 在冒泡排序中,当有相邻的两个元素大小相等的时候不做交换,因此相同大小的数据在排序前后顺序保持不变,所以冒泡排序是稳定的排序算法
  • 当数据已经是有序的,那么时间复杂度是 O(n);当数据刚好是倒序的,那么时间复杂度为 O(n^2);平均情况下的时间复杂度基本上是 O(n^2)

插入排序

插入排序:将数组中的元素分为已排序区间和未排序区间,排序的时候把未排序区间中的元素在已排序区间中找到合适的位置插入,并保证已排序区间数据一直有序,然后重复这个过程,直到未排序区间中元素为空。如下图所示(左侧为已排序区间,右侧为未排序区间):

插入排序包含两步操作,先是比较元素大小,再然后移动元素。将一个元素 a 插入到已排序区间时,需要拿 a 与已排序区间的元素依次比较大小,然后找到合适的插入位置,再然后将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素 a 插入。参考代码如下:

// 插入排序,a表示数组,n表示数组大小
func InsertionSort(a []int, n int) {if n <= 1 {return}for i := 1; i < n; i++ {value := a[i]j := i - 1//查找要插入的位置并移动数据for ; j >= 0; j-- {if a[j] > value {a[j+1] = a[j]} else {break}}a[j+1] = value//fmt.Println(a)}
}func main() {arr := []int{8, 5, 6, 3, 1, 7}InsertionSort(arr, len(arr))fmt.Println("插入排序后:", arr) // [1 3 5 6 7 8]
}

关于插入排序的细节分析: 

  • 插入排序不需要额外的存储空间,所以空间复杂度是 O(1),是一个原地排序算法
  • 插入排序中对于值相同的元素,可以选择将后面出现的元素插入到前面出现元素的后面,就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法
  • 当数据已经是有序的,那么时间复杂度是 O(n);当数据刚好是倒序的,那么时间复杂度为 O(n^2);平均情况下的时间复杂度基本上是 O(n^2)

选择排序

选择排序和插入排序有点类似,也分为 已排序区间 和 未排序区间,只不过 选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。如下图所示:

参考代码:

// 选择排序,a表示数组,n表示数组大小
func SelectionSort(a []int, n int) {if n <= 1 {return}for i := 0; i < n; i++ {// 查找最小值minIndex := ifor j := i + 1; j < n; j++ {if a[j] < a[minIndex] {minIndex = j}}// 交换a[i], a[minIndex] = a[minIndex], a[i]//fmt.Println(a)}
}func main() {arr := []int{8, 5, 6, 3, 1, 7}SelectionSort(arr, len(arr))fmt.Println("选择排序后:", arr) // [1 3 5 6 7 8]
}

关于选择排序的细节分析: 

  • 选择排序空间复杂度是 O(1),是一个原地排序算法
  • 选择排序每次都要找剩余未排序元素中的最小值和前面的元素交换位置,这样破坏了稳定性,因此选择排序是不稳定的排序算法
  • 选择排序的时间复杂度是 O(n^2)

三种排序的效率比较:

是否原地排序是否稳定排序最好最坏平均
冒泡排序O(n)O(n^2)O(n^2)
插入排序O(n)O(n^2)O(n^2)
选择排序O(n^2)O(n^2)O(n^2)

【问】插入排序和冒泡排序的时间复杂度都是 O(n^2),为什么在开发中更倾向于使用插入排序算法 而不是 冒泡排序算法?

【答】冒泡排序交换数据时需要 3 个赋值操作,而插入排序移动数据只需要 1 个赋值操作,在运算量很大的时候,插入排序相对来说有一定优势。

源代码:sort1/SortDemo1.go · 浮尘/go-algo-demo - Gitee.com 

【每日一练:K 个一组翻转链表】

力扣25. K 个一组翻转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例1:输入:head = [1,2,3,4,5], k = 2,输出:[2,1,4,3,5]。

示例2:输入:head = [1,2,3,4,5], k = 3,输出:[3,2,1,4,5]。

思路1:使用递归,时间复杂度: O(N),空间复杂度: O(N/K)。

type ListNode struct {Next  *ListNodevalue interface{}
}func reverseKGroup1(head *ListNode, k int) *ListNode {count := 0cur := headvar tmp *ListNodefor cur != nil && count < k {count++cur = cur.Next}// 当前 k-group 压根没有k个node,那么直接保持这个k-group不动返回headif count < k {return head}// last是k+1个节点后的链表的翻转结果last := reverseKGroup1(cur, k)// 从第一个节点开始反转,第一个节点挂在last前面,把last换成第一个节点// 第二个节点挂在last前面,继续把last换成第一个节点,直到把k个节点都反转完for count = 0; count < k; count++ {tmp = headhead = head.Nexttmp.Next = lastlast = tmp}return last
}func main() {n5 := &ListNode{value: 5}n4 := &ListNode{value: 4, Next: n5}n3 := &ListNode{value: 3, Next: n4}n2 := &ListNode{value: 2, Next: n3}n1 := &ListNode{value: 1, Next: n2}n1.Print() //原链表: 1->2->3->4->5//reverseKGroup1(n1, 2).Print() //每2个节点一组翻转: 2->1->4->3->5reverseKGroup1(n1, 3).Print() //每3个节点一组翻转: 3->2->1->4->5
}

思路2:使用循环,时间复杂度: O(N),空间复杂度: O(1)。

func reverseKGroup2(head *ListNode, k int) *ListNode {dummy := &ListNode{Next: nil, value: -1}dummy.Next = headdummy2 := dummyfor {p := dummy2.Nextstart := pcount := 0for count < k && p != nil {p = p.Nextcount++}//如果少于k个节点,则不需要翻转if count < k {break}//k个节点后的那个节点last := p//一个一个连过去p = dummy2.Nextfor i := 0; i < k; i++ {next := p.Nextp.Next = lastlast = pp = next}//翻转后的结果dummy2.Next = last//当前的第k个数据就是先前的第一个数据dummy2 = start}return dummy.Next
}func main() {//reverseKGroup2(n1, 2).Print() //每2个节点一组翻转: 2->1->4->3->5reverseKGroup2(n1, 3).Print() //每3个节点一组翻转: 3->2->1->4->5
}

源代码:leetcode/ReverseKGroup.go · 浮尘/go-algo-demo - Gitee.com


http://www.ppmy.cn/news/104871.html

相关文章

算法02-入门算法枚举与模拟算法

文章目录 总结大纲要求枚举算法枚举思想枚举举例题目描述 统计因数题目描述 质数判定错误方法一&#xff1a;优化方法1&#xff1a; 用break实现优化优化方法2&#xff1a; sqrt(n) 题目描述 水仙花数题目描述 7744问题实现方法1优化方法2 题目描述 余数相同问题题目描述 特殊自…

C++虚假唤醒

概念&#xff1a; 虚假唤醒是指在使用条件变量时&#xff0c;线程被唤醒但条件并没有满足&#xff0c;导致线程执行错误的情况&#xff0c;这个过程就是虚假唤醒。 虚假唤醒弊端&#xff1a; 虚假唤醒会导致程序的正确性受到影响&#xff0c;因为唤醒的线程并没有满足条件&…

SCI 投稿论文入门 —— 2. 图片编辑(Visio / Origin)

目录 引言IEEE trans论文图片格式要求单栏图片双栏图片 论文中插入曲线图曲线图具体要求 论文中插入结构图曲线图与结构图结合visio中设置界面单栏单张图片曲线图中需要插入结构图 箭头&#xff0c;线段粗细设置字体下标 引言 由于特殊要求&#xff0c;需要用word版本进行编辑…

Vue事件处理

1. 事件绑定 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-width…

在Centos Stream 9上Docker的实操教程(三) - Docker容器数据卷

在Centos Stream 9上Docker的实操教程 - Docker容器数据卷 问题场景Docker容器数据卷简单介绍数据卷使用操作实例安装redis验证配置文件生效验证数据是否丢失 结语 问题场景 Docker容器我们可以理解就是微型的linux系统&#xff0c;在使用容器的时候自然会产生一系列数据文件&…

【python+appium】自动化测试

pythonappium自动化测试系列就要告一段落了&#xff0c;本篇博客咱们做个小结。 首先想要说明一下&#xff0c;APP自动化测试可能很多公司不用&#xff0c;但也是大部分自动化测试工程师、高级测试工程师岗位招聘信息上要求的&#xff0c;所以为了更好的待遇&#xff0c;我们还…

SM国密算法(一)

一、简介 国密即国家密码局认定的国产密码算法&#xff0c;包括&#xff1a;SM1&#xff08;SCB2&#xff09;、SM2、SM3、SM4、SM7、SM9、祖冲之密码算法&#xff08;ZUC) 等。 二、分别介绍 SM1、SM4、SM7、祖冲之密码&#xff08;ZUC&#xff09;是对称算法。 SM2、SM9是…

Java:FileOutputStream

操作本地文件的字节输出流&#xff0c;可以把程序中的数据写到本地文件中。 1.写入数据的三种方法 void write(int b)&#xff1a;一次写一个字节数据void write(byte[ ] b)&#xff1a;一次写一个字节数组数据void write(byte[] b, int off, int len)&#xff1a;一次写一个…