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

news2024/9/28 4:46:07

目录

【递归】

【排序】

冒泡排序

插入排序

选择排序

【每日一练: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 := false
		for 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 := i
		for 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  *ListNode
	value interface{}
}

func reverseKGroup1(head *ListNode, k int) *ListNode {
	count := 0
	cur := head
	var tmp *ListNode
	for cur != nil && count < k {
		count++
		cur = cur.Next
	}
	// 当前 k-group 压根没有k个node,那么直接保持这个k-group不动返回head
	if count < k {
		return head
	}
	// last是k+1个节点后的链表的翻转结果
	last := reverseKGroup1(cur, k)
	// 从第一个节点开始反转,第一个节点挂在last前面,把last换成第一个节点
	// 第二个节点挂在last前面,继续把last换成第一个节点,直到把k个节点都反转完
	for count = 0; count < k; count++ {
		tmp = head
		head = head.Next
		tmp.Next = last
		last = 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->5
	reverseKGroup1(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 = head
	dummy2 := dummy
	for {
		p := dummy2.Next
		start := p
		count := 0
		for count < k && p != nil {
			p = p.Next
			count++
		}
		//如果少于k个节点,则不需要翻转
		if count < k {
			break
		}
		//k个节点后的那个节点
		last := p
		//一个一个连过去
		p = dummy2.Next
		for i := 0; i < k; i++ {
			next := p.Next
			p.Next = last
			last = p
			p = next
		}
		//翻转后的结果
		dummy2.Next = last
		//当前的第k个数据就是先前的第一个数据
		dummy2 = start
	}
	return dummy.Next
}

func main() {
	//reverseKGroup2(n1, 2).Print() //每2个节点一组翻转: 2->1->4->3->5
	reverseKGroup2(n1, 3).Print() //每3个节点一组翻转: 3->2->1->4->5
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/586761.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

特征选择及特征提取

特征 什么是特征&#xff1a; 举个例子&#xff1a;一个妹子很好看&#xff0c;好看的在哪里&#xff1f;腿长&#xff08;特征1&#xff09;&#xff0c;白&#xff08;特征2&#xff09;&#xff0c;性格开朗&#xff08;特征3&#xff09; 那么可以概括为好看妹子的特征是…

修改element Plus的主题样式

安装element plus 安装icon pnpm install element-plus pnpm install element-plus/icons-vue main.ts配置 icon的使用https://element-plus.gitee.io/zh-CN/component/icon.html#%E7%BB%93%E5%90%88-el-icon-%E4%BD%BF%E7%94%A8 import { createApp } from vue import ./sty…

用chatGPT来NEW个对象让“码农”的节日不再仅仅只有1024(赶鸭子上架式的成长、无效不得不立的flag)

用chatGPT来NEW个对象让“码农”的节日不再仅仅只有1024 前言一、大部分的成长都是赶鸭子上架二、节日是为了告诉自己不孤单三、做不到也要立下的flag四、New个对象吧1.php定义一个科技工作者形象2.python定义一个科技工作者形象3.javascript定义一个科技工作者形象 总结 前言 …

Docker的简单使用

文章目录 Docker的简单使用Docker 是什么Docker的基本组成镜像&#xff08;image&#xff09;容器&#xff08;container&#xff09;仓库&#xff08;repository&#xff09; 安装Docker卸载docker配置docker镜像加速Docker的常用命令docker安装nginx&#xff08;docker简单使…

chatgpt赋能python:Python中升序排序详解

Python中升序排序详解 什么是升序排序&#xff1f; 升序排序指的是按照从小到大的顺序排列数组、列表等数据类型。在Python中&#xff0c;可以使用各种函数和方法来对数据进行升序排序&#xff0c;例如sort()函数、sorted()函数、和lambda表达式等。下面将详细介绍这些方法。…

Leaflet基本用法

使用 阿里云地理工具 获取相应的地理JSON数据&#xff0c;用于对地图边界绘制。 如何使用leaflet&#xff1f; 这里用HTML5进行操作&#xff1b; 因为我是用的是Leaflet库&#xff0c;所以要引入JavaScript 和 CSS 文件&#xff08;可参考官网https://leafletjs.com/&#x…

chatgpt赋能python:Python中常用的内置函数

Python中常用的内置函数 Python是一门非常强大的编程语言&#xff0c;它有很多内置函数可以帮助开发人员更快速、更便捷地编写程序。在本文中&#xff0c;将会介绍并着重标记加粗一些常用的Python内置函数。 print() print()是Python中最基本也是最常用的内置函数之一&#…

【异常捕获】

异常捕获 异常概念处理错误方式 异常处理举例栈展开异常规范异常继承层次优缺点 异常 概念 异常时程序可能检测到的&#xff0c;运行时不正常的情况&#xff0c;如存储空间耗尽&#xff0c;数组越界等&#xff0c;可以预见可能发生在什么地方但不知道在什么时候发生的错误。 …

chatgpt赋能python:Python中如何更新pip:一篇详细指南

Python中如何更新pip&#xff1a;一篇详细指南 作为一个有10年Python编程经验的工程师&#xff0c;我很清楚更新pip的重要性。pip是Python的依赖管理工具&#xff0c;它可以帮助您轻松安装、升级和删除Python包。随着Python不断发展和更新&#xff0c;保持最新版本的pip也很重…

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

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

Springboot整合Swagger2(3.0.0版本)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

SpringCloudAlibaba下篇(GateWay,Skywalking)(超级无敌认真好用,万字收藏篇!!!!)

文章目录 SpringCloudAlibaba下篇(GateWay,Skywalking)1 GateWay1.1 什么是网关1.2 GateWay介绍1.3 GataWay的基本使用1.4 GataWay整合Nacos1.5 断言路由工厂1.5.1 内置断言路由工厂1.5.2 自定义断言路由工厂 1.6 过滤器工厂1.6.1 内置局部过滤器工厂1.6.2 自定义局部过滤器1.6…

手撕code(2)

文章目录 1 设计模式1.1 单例模式1.1.1 懒汉单例1.1.2 饿汉单例1.1.3 总结 1.2 简单工厂模式 2 实现智能指针 1 设计模式 1.1 单例模式 某个类&#xff0c;不应该有多个实例&#xff0c;此时就可以使用单例模式。如果尝试创建多个实例&#xff0c;编译器就会报错。 1.1.1 懒…

【踩坑】mirai挂机运行经常自动退出怎么办?

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 目录 背景介绍 解决思路 实现方法 最终效果 背景介绍 就是说&#xff0c;后台运行了mcl&#xff0c;但经常莫名其妙自动会退出&#xff0c;导致每次都得手动的去服务器上重新启动mcl。而对于自己运行的需要用…

“老年养生”APP的设计与开发

摘要&#xff1a;我国人口老龄化呈上升趋势&#xff0c;老年人口比重增加。这是我国经济发展的一大挑战&#xff0c;也是老年健康产业的一大机遇。随着我国经济发展&#xff0c;越来越多的人开始关注自己的身体&#xff0c;这导致各种关于健康的网络应用层出不穷。但是经过分析…

正则表达式与通配符 -- *?在正则表达式与通配符中的区别

1、前言 最近因为工作需要写一些自动化脚本&#xff0c;里面需要用到正则表达式来匹配特定的字符串&#xff0c;于是查了一些正则表达式相关的资料。资料里面都提到&#xff1a;*匹配前面的子表达式0次或任意多次。我当时就纳闷&#xff0c;*到底是表示的是匹配的次数还是可以…

2. JVM内存模型深度剖析与优化

JVM性能调优 1. JDK的体系结构2. Java语言的跨平台特性3.JVM整体结构及内存模型3.1 内存模型3.1.1 PC寄存器&#xff08;线程私有&#xff09;3.1.2 虚拟机栈&#xff08;线程私有&#xff09;1. 局部变量表2. 操作数栈 本文是按照自己的理解进行笔记总结&#xff0c;如有不正确…

SimpleCG绘图函数(3)--绘制矩形

前面我们已经学习了点和线的绘制,本篇我们继续绘图函数学习----矩形的绘制&#xff0c;也就是长方形的绘制,并给出一个绘制楼房的例子演示矩形的应用。 所有绘制矩形相关函数如下&#xff1a; //以下矩形左上角坐标(left, top)&#xff0c;右下角坐标(right,bottom ) //以线条…

跨境电商系统开发-电商商城系统平台定制方案

随着业务的拓展&#xff0c;不少企业开始将目光转向国外市场&#xff0c;那么如何定制一套属于想自己的跨境出海电商商城方案呢&#xff1f;需要做好以下关口把关&#xff1a; 欢迎名片交流探讨开发平台流程 买家端(h5/pc/app) www.mardao.cn 账号 密码 卖家端(h5/pc)…

八、(重点)视图集ModelViewSet自定义action路由routers

上一章&#xff1a; 七、Django DRF框架GenericAPIView--搜索&排序&分页&返回值_做测试的喵酱的博客-CSDN博客 下一章&#xff1a; 九、DRF生成API文档_做测试的喵酱的博客-CSDN博客 一、视图集ModelViewSet与ReadOnlyViesSet ModelViewSet视图集 与 ReadOnly…