数据结构是计算机科学中研究数据的组织、管理和存储方式的技术。它通常涉及到数据的组织方式、访问方法、操作和处理方式等方面的设计和实现。数据结构是计算机程序设计的基础,它直接影响了程序的运行效率和存储空间的利用率。
常见的数据结构包括数组、链表、栈、队列、树、图等。这些数据结构可以分为线性结构和非线性结构。线性结构中的数据元素之间存在一种线性关系,如数组、链表、栈和队列;非线性结构中的数据元素之间没有简单的顺序关系,如树和图。
数据结构的选择和设计往往取决于问题的特性和需要解决的具体问题。不同的数据结构适用于不同的场景。例如,数组适用于顺序存储和随机访问,链表适用于动态操作和插入删除,树适用于层次结构和递归操作,图适用于表示复杂的关系和网络。在实际应用中,需要根据问题的特性选择合适的数据结构,以提高程序的效率和性能。
数据类型
抽象数据类型
时间复杂度
算法时间效率的比较
空间复杂度
集合、列表和数组概念
本文中介绍的概念为适用于所有编程语言的抽象理论,具体实现会由编程语言的不同而稍有差别。
具体介绍数组之前,我们先来了解一下集合、列表和数组的概念之间的差别。
集合
集合一般被定义为:由一个或多个确定的元素所构成的整体。
通俗来讲,集合就是将一组事物组合在一起。你可以将力扣的题库看作一个集合:
也可以将力扣商店里的礼品看作一个集合:
甚至可以将桌面上的物品当作一个集合。
集合有什么特性呢?
首先,集合里的元素类型不一定相同。 你可以将商品看作一个集合,也可以将整个商店看作一个集合,这个商店中有人或者其他物品也没有关系。
其次,集合里的元素没有顺序。 我们不会这样讲:我想要集合中的第三个元素,因为集合是没有顺序的。
事实上,这样的集合并不直接存在于编程语言中。然而,实际编程语言中的很多数据结构,就是在集合的基础上添加了一些规则形成的。
列表
列表(又称线性列表)的定义为:是一种数据项构成的有限序列,即按照一定的线性顺序,排列而成的数据项的集合。
列表的概念是 在集合的特征上形成的,它具有顺序,且长度是可变的你可以把它看作一张购物清单:
在这张清单中:
购物清单中的条目代表的类型可能不同,但是按照一定顺序进行了排列;
购物清单的长度是可变的,你可以向购物清单中增加、删除条目。
在编程语言中,
列表最常见的表现形式有数组和链表,而我们熟悉的栈和队列则是两种特殊类型的列表
。除此之外,向列表中添加、删除元素的具体实现方式会根据编程语言的不同而有所区分。
数组
数组是列表的实现方式之一,也是面试中经常涉及到的数据结构。
正如前面提到的,数组是列表的实现方式,它具有列表的特征,同时也具有自己的一些特征。然而,在具体的编程语言中,数组这个数据结构的实现方式具有一定差别。比如 C++ 和 Java 中,数组中的元素类型必须保持一致,而 Python 中则可以不同。Python 中的数组叫做 list,具有更多的高级功能。
那么如何从宏观上区分列表和数组呢?这里有一个重要的概念:索引。
首先,数组会用一些名为 索引 的数字来标识每项数据在数组中的位置,且在大多数编程语言中,索引是从 0 算起的。我们可以根据数组中的索引,快速访问数组中的元素。
而列表中没有索引,这是数组与列表最大的不同点。
其次,数组中的元素在内存中是连续存储的,且每个元素占用相同大小的内存。要理解这一点,我们需要了解数组在内存中的存储方式,我们将在下一节中详细介绍。
相反,列表中的元素在内存中可能彼此相邻,也可能不相邻。比如列表的另一种实现方式——链表,它的元素在内存中则不一定是连续的。有关链表的介绍,可以在探索卡片「链表」中进行进一步学习。
以上就是集合、列表和数组的介绍,你能自己总结出它们的不同点吗?
数组的操作
本节我们重点来讲解一下数组的 4 种操作。
读取元素
读取数组中的元素,是通过访问索引的方式来读取的,索引一般从 0 开始。
在计算机中,内存可以看成一些已经排列好的格子,每个格子对应一个内存地址。一般情况下,数据会分散地存储在不同的格子中。
而对于数组,计算机会在内存中为其申请一段 连续 的空间,并且会记下索引为 0 处的内存地址。以数组 [“C”, “O”, “D”, “E”, “R”] 为例,它的各元素对应的索引及内存地址如下图所示。
假如我们想要访问索引为 2 处的元素 “D” 时,计算机会进行以下计算:
找到该数组的索引 0 的内存地址: 2008;
将内存地址加上索引值,作为目标元素的地址,即 2008 + 2 = 2010,对应的元素为 “D”,这时便找到了目标元素。
我们知道,计算内存地址这个过程是很快的,而我们一旦知道了内存地址就可以立即访问到该元素,因此它的时间复杂度是常数级别,为 O(1)。
查找元素
假如我们对数组中包含哪些元素并不了解,只是想知道其中是否含有元素 “E”,数组会如何查找元素 `“E” 呢?
与读取元素类似,由于我们只保存了索引为 0 处的内存地址,因此在查找元素时,只需从数组开头逐步向后查找就可以了。如果数组中的某个元素为目标元素,则停止查找;否则继续搜索直到到达数组的末尾。
我们发现,最坏情况下,搜索的元素为 “R”,或者数组中不包含目标元素时,我们需要查找 n 次,n 为数组的长度,因此查找元素的时间复杂度为 O(N),𝑁为数组的长度
插入元素
假如我们想在原有的数组中再插入一个元素 “S” 呢?
如果要将该元素插入到数组的末尾,只需要一步。即计算机通过数组的长度和位置计算出即将插入元素的内存地址,然后将该元素插入到指定位置即可。
然而,如果要将该元素插入到数组中的其他位置,则会有所区别,这时我们首先需要为该元素所要插入的位置 腾出 空间,然后进行插入操作。比如,我们想要在索引 2 处插入 “S”。
我们发现,如果需要频繁地对数组元素进行插入操作,会造成时间的浪费。事实上,另一种数据结构,即链表可以有效解决这个问题,我们将在另外的卡片中进行学习。
删除元素
删除元素与插入元素的操作类似,当我们删除掉数组中的某个元素后,数组中会留下 空缺 的位置,而数组中的元素在内存中是连续的,这就使得后面的元素需对该位置进行 填补 操作。
以删除索引 1 中的元素 “O” 为例,具体过程如图所示。
当数组的长度为 n 时,最坏情况下,我们删除第一个元素,共需要的步骤数为 1 + (n - 1) = n 步,其中,1 为删除操作,n - 1 为移动其余元素的步骤数。删除操作具有线性时间复杂度,即时间复杂度为O(N),N 为数组的长度。
例题一:寻找数组的中心下标
给你一个整数数组 nums ,请计算数组的 中心下标 。
数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。
如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1 。
示例 1:
输入:nums = [1, 7, 3, 6, 5, 6]
输出:3
解释:
中心下标是 3 。
左侧数之和 sum = nums[0] + nums[1] + nums[2] = 1 + 7 + 3 = 11 ,
右侧数之和 sum = nums[4] + nums[5] = 5 + 6 = 11 ,二者相等。
示例 2:
输入:nums = [1, 2, 3]
输出:-1
解释:
数组中不存在满足此条件的中心下标。
示例 3:
输入:nums = [2, 1, -1]
输出:0
解释:
中心下标是 0 。
左侧数之和 sum = 0 ,(下标 0 左侧不存在元素),
右侧数之和 sum = nums[1] + nums[2] = 1 + -1 = 0 。
代码:
方法一
class Solution {
public int pivotIndex(int[] nums) {
int i=0;
// int y=-1;
// System.out.println(nums.length);
for( i=0;i<nums.length;i++){。;
if(i==0){
int num1=0;
for(int j=nums.length-1;j>0;j--){
num1=num1+nums[j];
}
if(num1==0){
// y=i;
// System.out.println("中心下标是:"+i);
return i;
}
}
if(i!=0){
if(i > 0){
int num2=0,num3=0;
for(int e=0;e<i;e++){num3=num3+nums[e];}
for(int j=nums.length-1;j>i;j--){num2=num2+nums[j];}
// System.out.println(num3+"+"+num2);
if(num3==num2){
// y=i;
// System.out.println("中心下标是:"+i);
return i;
}
}
}
}
return -1;
}
}
方法二
首先计算整个数组的总和,然后遍历数组,每次更新左侧元素的和,同时检查左侧元素的和是否等于右侧元素的和(即总和减去左侧元素的和再减去当前元素)。如果相等,则返回当前下标。如果遍历结束后仍未找到中心下标,则返回 -1。
class Solution {
public int pivotIndex(int[] nums) {
int totalSum = 0;
for (int num : nums) {
totalSum += num;
}
int leftSum = 0;
for (int i = 0; i < nums.length; i++) {
if (leftSum == totalSum - leftSum - nums[i]) {
return i;
}
leftSum += nums[i];
}
return -1;
}
}
方法三
因为左、右两侧的元素之和相等,所以2*prefix=sum - nums[i]等于数组元素总和减去当前元素
class Solution {
public int pivotIndex(int[] nums) {
int sum = 0;
for(int num:nums) sum += num;
int prefix = 0;
for(int i =0; i< nums.length; i++){
if(prefix * 2 == sum - nums[i]) return i;
prefix += nums[i];
}
return -1;
}
}
例题二:搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
代码:
方法一
class Solution {
public int searchInsert(int[] nums, int target) {
int i;
for( i=0;i<nums.length;i++){
if(nums[i]==target) return i;
if(target>nums[i]&&target<nums[nums.length-1]&&target<nums[i+1]) return ++i;
if(target<nums[0]) return 0;
}
return i;
}
}
**推荐:**
方法二:**二分查找算法**的实现,用于在一个有序数组中查找目标值的位置。如果目标值存在于数组中,则返回其索引;如果不存在,则返回应该插入的位置以保持数组有序。
解析:
初始化两个指针 i 和 j,分别指向数组的第一个元素和最后一个元素。
当 i <= j 时,执行循环。
计算中间位置 m = (i + j) / 2。
如果目标值小于中间位置的值,说明目标值在左侧,更新 j = m - 1。
如果目标值大于中间位置的值,说明目标值在右侧,更新 i = m + 1。
如果目标值等于中间位置的值,返回中间位置的索引 m。
循环结束后,如果没有找到目标值,返回 i,即应该插入的位置。
class Solution {
public int searchInsert(int[] a, int target) {
int i=0,j=a.length-1;
while (i<=j){
int m=(i+j)>>>1;
if(target<a[m]){ //目标值在左侧
j=m-1;
}else if(a[m]<target){ //目标值在右侧
i=m+1;
}else {
return m;
}
}
return i;
}
}
方法三:
class Solution {
public int searchInsert(int[] nums, int target) {
for(int i=0;i<nums.length;i++){
if(nums[i]>=target) return i;
}
return nums.length;
}
}
例题三:合并区间
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
代码:
class Solution {
public int[][] merge(int[][] intervals) {
if (intervals.length == 0){
return new int[0][2];
}
//Arrays.sort()方法遍历区间数组通过比较器给区间数组排序,两个参数intervals(区间数组)和匿名内部类实现接口Comparator(比较器)重写方法compare方法比较元素间的起始值并返回整数值
Arrays.sort(intervals,new Comparator<int[]>(){
public int compare(int[] interval1,int[] interval2){
return interval1[0] - interval2[0];
}
});
//建立一个集合便于动态扩容对重合的区间进行合并重新排序
List<int[]> merged = new ArrayList<int[]>();
for (int i =0;i<intervals.length;i++){
int L = intervals[i][0],R=intervals[i][1];
if(merged.size()==0 || merged.get(merged.size()-1)[1] < L){
merged.add(new int [] {L, R});
}else{
merged.get(merged.size()-1)[1]=Math.max(merged.get(merged.size()-1)[1],R);
}
}
//最后把集合通过方法toArray转化为数组(把集合merged的值复制到行为merged.size()的二维数组中返回
return merged.toArray(new int [merged.size()][]);
}
}
作者:LeetCode
链接:https://leetcode.cn/leetbook/read/array-and-string/c5tv3/
来源:力扣(LeetCode)