我们已经正式进入第三关(数组篇)了,从本章节开始,就开始啃数组这块儿大骨头了,不知道前面链表这块"小骨头" hxdm 有没有消化掉啊,没有的话,再沉淀沉淀吧,要不然容易撑着(贪多嚼不烂)或者拉肚子(学后面忘前面)。当然能走到这里的,都是付出时间和努力的。(艹,又跑题了,赶紧拉回正轨!!!)
进入正题,在学习数组之前,我们先来复习一下常见的数据结构。这里只是简单的从几个方面来介绍各个数据结构,后续会出一个章节来搞个全面的数据结构或算法的总结,浅浅的期待一下吧!(废话太多了,以后直接切入主题,不再做铺垫,哎!)
常见的数据结构
1. 数组(Array):
- 特点:由相同数据类型的元素组成,连续存储在内存中。可以通过下标访问元素,查找速度快,但插入和删除元素较慢。
- 应用:适用于索引访问元素,对元素的增删操作不频繁的情况。2. 链表(Linked List):
- 特点:由节点组成,每个节点包含一个数据元素和一个指向下一个节点的指针。分为单向链表、双向链表和循环链表。
- 应用:适用于频繁插入和删除元素的场景。3. 栈(Stack):
- 特点:后进先出(LIFO)的数据结构,只允许在栈顶进行插入和删除操作。
- 应用:适用于递归、表达式求值、回溯等场景。4. 队列(Queue):
- 特点:先进先出(FIFO)的数据结构,允许在队列的一端插入元素,在另一端删除元素。
- 应用:适用于广度优先搜索、任务调度等场景。5. 哈希表(Hash Table):
- 特点:通过哈希函数将键映射到数组的索引,实现快速查找、插入和删除操作。
- 应用:适用于快速查找、去重、计数等场景。6. 树(Tree):
- 特点:由节点组成,每个节点包含一个数据元素和若干子节点。常见的树结构有二叉树、二叉搜索树、AVL树等。
- 应用:适用于层次关系、搜索和排序等场景。7. 堆(Heap):
- 特点:是一种特殊的二叉树,满足堆序性(最大堆或最小堆),堆中的节点值满足某种特定的顺序关系。
- 应用:适用于优先级队列、求Top K元素等场景。8. 图(Graph):
- 特点:由节点和边组成,节点表示数据元素,边表示节点之间的关系。
- 应用:适用于表示复杂关系、图算法等场景。9. 字典树(Trie):
- 特点:一种多叉树结构,用于高效地存储和搜索字符串集合。
- 应用:适用于字典搜索、自动补全等场景。
线性表
这里我们只重点介绍与本章节有关的线性表,也就是我们前面学的链表和本章节的数组,其它的之后再学!
线性表是一种常见的数据结构,它是由一组有序的元素组成,这些元素之间存在一对一的关系。线性表中的元素按照线性的顺序排列,每个元素都有一个唯一的前驱元素和一个唯一的后继元素(除了第一个元素没有前驱元素,最后一个元素没有后继元素)。
线性表有两种常见的实现方式:数组和链表。
1. 数组实现线性表:
- 特点:数组是一种连续的内存空间,元素按照顺序存储在数组中,可以通过下标直接访问元素。插入和删除操作较慢,需要移动其他元素,但查找元素速度较快。
- 应用:适用于元素的索引访问和静态大小的线性表。2. 链表实现线性表:
- 特点:链表是一种离散的内存空间,每个元素(节点)包含数据和指向下一个节点的指针。插入和删除操作较快,只需修改指针,但查找元素需要遍历链表。
- 分类:链表可以分为单向链表、双向链表和循环链表。
- 应用:适用于频繁插入和删除元素的场景。线性表的操作包括:插入、删除、查找、获取元素等。线性表常见的操作时间复杂度如下:
- 在数组中插入元素:O(n)
- 在数组中删除元素:O(n)
- 在数组中查找元素:O(n)
- 在链表中插入元素:O(1)
- 在链表中删除元素:O(1)
- 在链表中查找元素:O(n)线性表的应用非常广泛,例如列表、队列、栈等都是线性表的具体应用。线性表为我们提供了一种简单高效的数据结构,帮助我们组织和操作数据,解决实际问题。
什么是数组
数组是一种线性数据结构,它由一组相同类型的元素组成,这些元素在内存中按照一定的顺序连续存储。数组是一种简单且常见的数据结构,它可以在内存中分配一块连续的存储空间来存放多个元素,并通过下标(索引)来访问和操作元素。
数组有以下特点:
1. 由相同类型的元素组成:数组中的元素必须具有相同的数据类型,可以是整数、浮点数、字符、对象等。
2. 连续存储:数组中的元素在内存中是连续存储的,这样可以通过计算偏移量来快速访问元素。
3. 固定大小:数组的大小在创建时就确定了,一旦创建后,大小不能改变。如果需要更改大小,需要重新创建一个新的数组。
4. 下标访问:数组中的元素可以通过唯一的下标来访问,下标从0开始,逐个增加,最后一个元素的下标为数组大小减1。数组的访问操作是非常高效的,因为可以通过下标直接计算出元素的地址,而不需要遍历整个数组。但是插入和删除元素的操作比较低效,因为需要将插入位置后面的元素向后移动或将删除位置后面的元素向前移动。
数组在计算机编程中应用广泛,常用于存储和处理一组数据,例如存储学生成绩、温度数据、图像像素等。同时,数组也是其他复杂数据结构的基础,例如链表、栈、队列等都可以用数组来实现。
数组存储元素的特征
1. 相同类型:数组中的元素必须具有相同的数据类型。例如,如果一个数组是整数类型的,那么其中的所有元素都必须是整数,不能混合存放其他类型的数据。
2. 连续存储:数组中的元素在内存中是连续存储的。数组的元素在内存中按照一定的顺序依次存放,可以通过计算元素的偏移量来访问特定位置的元素。
3. 固定大小:数组在创建时就需要指定其大小,一旦创建后,大小是固定不变的。如果需要存储更多或更少的元素,需要重新创建一个新的数组。
4. 下标访问:数组中的元素可以通过唯一的整数下标来访问。数组的下标从0开始,逐个增加,最后一个元素的下标为数组大小减1。
5. 随机访问:由于数组中的元素是连续存储的,因此可以通过下标直接计算元素的地址,实现快速随机访问。数组的时间复杂度为O(1)。
6. 有序集合:数组是有序的,其中的元素按照数组中的位置顺序排列。可以根据元素在数组中的下标来确定其在有序集合中的位置。
数组是一种简单且高效的数据结构,常用于存储一组相同类型的数据,并且支持快速随机访问。然而,由于其大小固定和插入/删除操作低效的特点,有时候可能不适合处理动态数据集合。在这种情况下,其他数据结构如链表可能更为合适。
数组的增删改查以及初始化
初始化
Java中初始化数组非常简单,如下:
int[] arr = new int[6];
赋值
第一种:使用数组初始化器,即在声明数组时,使用数组初始化器直接给数组元素赋值。数组初始化器是一种简洁的语法,适用于在编译时已知数组元素值的情况。
int[] arr = {1,2,3,4,5,6};
第二种:循环赋值
int[] arr = new int[6];
for(int i = 0;i < arr.length;i++){
arr[i] = i + 1;
}
查找一个元素
许多算法问题本质上都是查找问题。例如滑动窗口问题、回溯问题、动态规划问题等等,都需要在搜索过程中寻找目标结果。在这些问题中,我们通常会使用数组来辅助存储和处理数据,因为数组的高效查找特性可以帮助我们更快地找到目标结果。 因此,熟练掌握数组的操作和查找算法是解决算法题目的关键,也是建立算法思维的重要一步。同时,学习其他高级数据结构和算法也是提高解题能力和算法效率的重要途径。
下面我就来看看查找的方法:
1.线性搜索: 线性搜索是一种简单的查找方法,逐个遍历数组元素,直到找到目标元素或遍历完整个数组。
代码如下:
public int linearSearch(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i; // 返回目标元素的索引
}
}
return -1; // 如果找不到目标元素,则返回-1
}
2.二分搜索: 二分搜索是一种高效的查找方法,前提是数组必须是有序的。它通过反复将数组分成两半,并根据目标元素与中间元素的大小关系,决定继续搜索的方向。
这个方法是一个非常重要且容易出错的,后续会展开来讲!
代码如下:
public int binarySearch(int[] arr, int target) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid; // 找到目标元素,返回索引
} else if (arr[mid] < target) {
left = mid + 1; // 目标元素在右半部分,更新左边界
} else {
right = mid - 1; // 目标元素在左半部分,更新右边界
}
}
return -1; // 如果找不到目标元素,则返回-1
}
增加一个元素
看似简答,实则内有乾只因!
代码如下:
public static int addByElementSequence(int[] arr, int size, int element) {
// 判断数组是否已满
if (size >= arr.length) {
return -1;
}
int index = size; // 初始化插入位置为 size,表示在数组末尾插入元素
// 找到新元素的插入位置
for (int i = 0; i < size; i++) {
if (element < arr[i]) {
index = i;
break;
}
}
// 元素后移,从数组最后一个元素开始移动,依次向后移动一个位置
for (int j = size; j > index; j--) {
arr[j] = arr[j - 1]; // index 下标开始的元素后移一个位置
}
arr[index] = element; // 插入数据
return index;
}
删除一个元素
对于删除不能和添加一样,因为可能你要删除的元素根本就不存在,所以要先查是否有要删除的元素,有的化再删除。
首先,我们定义一个
index
变量,并将其初始化为-1
。这个变量用于记录需要移除的元素在数组中的位置,默认情况下我们假设元素不存在,所以初始化为-1
。然后,我们遍历数组,找到需要移除的元素的位置。在遍历过程中,我们比较数组中的元素和指定的
key
,如果找到了相同的元素,则更新index
为该元素在数组中的位置,并立即跳出循环。接下来,我们判断
index
是否为-1
,即是否找到了需要移除的元素。如果index
不为-1
,说明找到了需要移除的元素,我们需要进行移除操作。移除操作是将数组中的元素依次向前移动一个位置,以覆盖需要移除的元素。我们从
index + 1
开始,将后面的元素依次向前移动一个位置,这样就覆盖了需要移除的元素。最后,我们将数组的大小
size
减一,表示数组中的元素个数减少了一个,即成功移除了一个元素。
代码如下:
public int removeByElement(int[] arr, int size, int key) {
int index = -1;
for (int i = 0; i < size; i++) {
if (arr[i] == key) {
index = i;
break;
}
}
if (index != -1) {
for (int i = index + 1; i < size; i++) {
arr[i - 1] = arr[i];
}
size--;
}
return size;
}
来两道题目先练练手
单调数列
如果数组是单调递增或单调递减的,那么它是 单调 的。
如果对于所有
i <= j
,nums[i] <= nums[j]
,那么数组nums
是单调递增的。 如果对于所有i <= j
,nums[i]> = nums[j]
,那么数组nums
是单调递减的。当给定的数组
nums
是单调数组时返回true
,否则返回false
。输入:nums = [1,2,2,3] 输出:true
思路:遍历数组,比较当前和后一个数组的大小,但要注意边界问题
代码如下:
public boolean isMonotonic(int[] nums){
boolean incr = true,desc = true;
int n = nums.length;
for(int i = 0;i < n - 1;i++){
if(nums[i] > nums[i + 1]){
incr = false;
}
if(nums[i] < i[i + 1]){
desc = false;
}
}
return incr || desc;
}
我们做任何事都不是白做的,往往都是带有一定的目的性。做上面的判断数组单调性也是一样,下面就是其的扩展使用。
搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
输入: nums = [1,3,5,6], target = 5 输出: 2
对于这类问题,存在好说,直接返回索引即可,不存在了不就用到了上面的单调问题了。当然这是一种常规的思路,写出答案还是没有问题的。但是对于此类问题还有一种更加简单高效的方法,即二分查找,对于在单调序列中查找的情况是一种不二的选择。
这里就以二分法为例,具体的使用场景后面会介绍:
public int searchInsert(int[] nums,int target){
int n = nums.length;
int left = 0,right = n - 1,res = n;
while(left <= right){
int mid = ((right-left)>>1) + left;
if(target <= nums[mid]){
res = mid;
right = mid - 1;
}else{
left = mid + 1;
}
}
return res;
}
合并两个有序数组
给你两个按 非递减顺序 排列的整数数组
nums1
和nums2
,另有两个整数m
和n
,分别表示nums1
和nums2
中的元素数目。请你 合并
nums2
到nums1
中,使合并后的数组同样按 非递减顺序 排列。注意:最终,合并后数组不应由函数返回,而是存储在数组
nums1
中。为了应对这种情况,nums1
的初始长度为m + n
,其中前m
个元素表示应合并的元素,后n
个元素为0
,应忽略。nums2
的长度为n
。输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 输出:[1,2,2,3,5,6] 解释:需要合并 [1,2,3] 和 [2,5,6] 。 合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
题虽不难,但更是展现自己出彩一面的机会。对于本题可以使用先将两个数组进行合并(把其中一个数组放到另一个后面),然后再进行排序的偷懒方法(doge)~~,也可以将一个数组一个个的插入到另一个中,还可以使用双指针。
// 把一个数组添加到另一个末尾再排序
public void merge(int[] nums1,int m,int[] nums2,int n){
for(int i = 0;i != ;++i){
nums1[m + i] = nums2[i];
}
Arrays.sort(nums1);
}
解法2
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m - 1;
int j = n - 1;
int k = m + n - 1;
while (i >= 0 && j >= 0) {
if (nums1[i] >= nums2[j]) {
nums1[k--] = nums1[i--];
} else {
nums1[k--] = nums2[j--];
}
}
while (j >= 0) {
nums1[k--] = nums2[j--];
}
}
// 逆向双指针
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m - 1;
int j = n - 1;
int k = m + n - 1;
int cur;
while(i >= 0 || j >= 0){
if(i == -1){
cur = nums2[j--];
}else if(j == -1){
cur = nums1[i--];
}else if(nums1[i] > nums2[j]){
cur = nums1[i--];
}else{
cur = nums2[j--];
}
nums1[k--] = cur;
}
}
结束!