一、数据结构基本介绍
什么是数据结构
数据结构,是计算机中组织、存储和管理数据的一套方式。现实世界里我们会把物品分类存放,方便查找、取用;在程序当中,数据同样需要合理的组织方式,这就是数据结构。
程序处理的本质就是对数据做操作:存储、查询、插入、删除、修改。不同的数据结构,在完成同样操作时,消耗的时间和内存是不一样的。学习数据结构,核心目的就是根据业务场景,选择合适的数据结构,优化程序的执行效率。
数据结构的分类
数据结构可以分为两大类别:线性结构与非线性结构。
1. 线性结构
数据元素之间是一对一的关系,排成一条直线。
常见:数组、链表、栈、队列。
数组:连续内存,支持下标随机访问;
链表:离散内存,依靠指针串联;
栈:后进先出(LIFO);
队列:先进先出(FIFO)。
2. 非线性结构
元素之间是一对多、多对多的关系。
常见:树、图、哈希表。
树:一对多,例如二叉树;
图:多对多,用来表示网络关系。
算法与数据结构的关系
数据结构是数据存放的容器,算法是处理数据的方法,二者相辅相成。
举个例子:在有序数组中查找目标值。
如果使用暴力遍历,从头到尾逐个对比,时间复杂度 O(n);
如果使用二分查找算法,每次直接排除一半数据,时间复杂度优化到 O(\log n)。
数据结构提供存储载体,算法负责高效操作数据。
学习顺序:先掌握基础线性结构(数组、链表),再学习栈、队列,之后再学习树、图等复杂结构。
二、数组基本概念
数组的定义
数组是一种连续的、固定大小的线性数据结构,在内存中开辟一块连续的存储空间,存放一组相同类型的数据。
数组核心特性
1. 内存连续
数组的所有元素在内存地址上是紧挨着的,没有空隙。这是数组最重要的特点。
正因为内存连续,数组可以通过下标直接定位元素,实现随机访问,访问任意下标元素的时间复杂度为 O(1)。
2. 下标从0开始
绝大多数编程语言(C/C++、Python、Java)数组下标起始为0。
数组第一个元素下标为0,第二个下标为1,以此类推。
对于长度为n的数组,合法下标范围是 0 \sim n-1。
注意:下标不能越界,如果访问下标等于数组长度,就会发生数组越界错误。
3. 长度固定
静态数组一旦创建,数组的总长度就不能改变。
如果想要存放更多元素,只能重新开辟一块更大的内存空间,把旧数组的数据复制过去。Python中的list虽然可以动态append,底层本质也是数组扩容机制。
数组与二分查找的关系
二分查找能够生效,前提条件是数组必须有序。
数组拥有随机访问能力,我们可以快速拿到中间位置的元素,不断缩小查找区间。如果是无序数组,无法使用二分查找,只能暴力遍历。
LeetCode704题目给出的就是升序数组,正好适合二分查找。
三、二分查找两种区间写法解题思路
本题为有序数组的目标值查找,利用二分查找可以将时间复杂度优化至 O(log n)。根据区间定义不同,分为左闭右闭和左闭右开两种标准写法,核心逻辑为不断缩小查找区间,直至找到目标或区间为空。
一、写法一:左闭右闭区间 [left, right]
1. 区间定义
查找区间为 左右边界均包含 的有效下标区间。
初始化:左指针 left = 0 ,右指针 right = len(nums) - 1 ,区间内所有下标都是合法查找范围。
2. 循环条件
循环条件设置<= right 。 原因:当 left == right 时,区间内仍保留一个有效元素,需要进入循环判断;只有 left > right` 时,区间彻底为空,查找结束。
3. 区间收缩逻辑
每次取中间位置 mid = left + (right - left) // 2 ,避免数值溢出:
1. 若 nums[< target :目标值在右侧区间,mid 位置已排除,更新左边界 left = mid + 1
2. 若 nums[mid] > target :目标值在左侧区间,mid 位置已排除,更新右边界 right = mid - 1
3. 若 nums[mid] == target :找到目标元素,直接返回当前下标 mid
4. 结果处理
循环正常退出说明区间内无目标值,返回 -1。
复杂度
时间复杂度:O(log n),每次查找区间长度减半
空间复杂度:O(1),仅使用常数变量
二、写法二:左闭右开区间 [left, right)
1. 区间定义
查找区间为 包含左边界、不包含右边界。
初始化:左指针 left = 0 ,右指针 right = len(nums) ,right 为数组边界外下标,不属于查找区间。
2. 循环条件
循环条件设置为 while left< right 。
原因:左、右指针相等时,区间 [left, right) 为空,无元素可判断,直接结束循环。
3. 区间收缩逻辑
同样取中间位置 mid = left + (right - left) // 2 :
1. 若 nums[mid]< target :目标在右侧,更新左边界 left = mid + 1
2. 若 nums[mid] > target :目标在左侧,因右边界为开区间,mid 本身不在区间内,直接更新右边界 right = mid
3. 若 nums[mid] == target :找到目标元素,返回下标 mid
4. 结果处理
循环结束未匹配到目标值,返回 -1。
复杂度
时间复杂度:O(log n)
空间复杂度:O(1)
总结
两种写法核心区别由区间定义决定,三者必须统一:
1. 左闭右闭: right=len-<=right 、 right=mid-1
2. 左闭右开: right=len <right 、 right=mid`
两种算法效率完全一致,仅边界处理逻辑不同。