六、数据结构
(一)数组
- 定义:数组是存放在连续内存空间上的相同类型数据的集合。数组可以方便的通过下标索引的方式获取到下标下对应的数据。
注意:
(1)数组下标都是从0开始的。
(2)数组内存空间的地址是连续的。正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
(3)使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。
(4)二维数组在内存的空间地址是连续的么?
答:不同编程语言的内存管理是不一样的,以C++为例,在C++中二维数组是连续分布的
1.数组之二分查找
例题1:二分查找
这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素。这是最简单的二分查找的题目。
题目 难度:简单
class Solution {
public:
int search(vector<int>& nums, int target)
{
int left = 0;
int right = nums.size() - 1;
while (left <= right)//截至条件相当于是区间的大小 < 1
//假如left==right,那么left==middle==right
{
int middle = left + ((right - left) / 2);
if(nums[middle]>target)//说明target在middle左边
{
right=middle-1;
}
else if(nums[middle]<target)//说明target在middle右边
{
left=middle+1;
}
else return middle;//nums[middle]==target
}
return -1;
}
};
例题2:数的范围
本质是:边界有一半区间满足条件,一半不满足。我们可以找到这两个区间的边界。相当于找绿色区域的左边界,找红色区域的右边界。
mid是中间的下标,x是要查找的数。
- 找左边界
如果mid满足绿色区域的条件(q[mid]>=x),那么从右侧逼近,right=mid,如果mid不满足绿色区域的条件,即mid在红色区域,那就从左侧逼近,left=mid+1; - 找右边界
如果mid满足红色区域的条件(q[mid]<=x),那么从左侧逼近,left=mid,如果mid不满足红色区域的条件,即mid在绿色区域,那就从右侧逼近,right=mid-1;
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int q[N];
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++) cin>>q[i];
while(m--)
{
int x;
cin>>x;//要查找的数
int left=0,right=n-1;
while(left<right)
{
int mid = (left + right) >> 1;
if(q[mid]>=x) right=mid;//向左逼近,找左边界 x...mid...
else left=mid+1;//mid...x...
}
if(q[left]!=x) cout<<"-1 -1"<<endl;
else
{
cout<<left<<" ";
int left = 0, right = n - 1;
while(left<right)
{
int mid=(left + right +1) >> 1;
if (q[mid] <= x) left = mid;//向右逼近,找右边界
else right = mid - 1;
}
cout<<right<<endl;
}
}
return 0;
}
例题3:在排序数组中查找元素的第一个和最后一个位置
题目 难度:中等
class Solution {
public:
vector<int> res={
-1,-1};
vector<int> searchRange(vector<int>& nums, int target)
{
int n=nums.size();
if(n==0) return{
-1,-1};
int left=0,right=n-1;
while(left<right)
{
int mid = (left + right) >> 1;
if(nums[mid]>=target) right=mid;//向左逼近,找左边界 x...mid...
else left=mid+1;//mid...x...
}
if(nums[left]!=target) return {
-1,-1};
else
{
res[0]=left;
int left = 0, right = n - 1;
while(left<right)
{
int mid=(left + right +1) >> 1;
if (nums[mid] <= target) left = mid;//向右逼近,找右边界
else right = mid - 1;
}
res[1]=left;
}
return res;
}
};
2.数组之移除元素
例题4:移除元素
题目 难度:中等
数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。
方法一:暴力
class Solution {
public:
int removeElement(vector<int>& nums, int val)
{
int len=nums.size();
for(int i=0;i<len;i++)
{
if(nums[i]==val) //找到要删除的元素了
{
for (int j = i + 1; j < len; j++)
{
nums[j - 1] = nums[j];
}
i--;
len--;
}
}
return len;
}
};
方法二:双指针
定义快慢指针
快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组。
慢指针:指向更新 新数组下标的位置。
class Solution {
public:
int removeElement(vector<int>& nums, int val)
{
int slow_index = 0;
//快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
//慢指针:指向更新 新数组下标的位置
for(int fast_index=0;fast_index<nums.size();fast_index++)
{
if (val != nums[fast_index]) //如果不是要移除的元素
{
nums[slow_index] = nums[fast_index];
slow_index++;
}
}
return slow_index;
}
};
(二)链表
1.链表之静态链表(用数组模拟单链表)
例题5:单链表
题目
注意:题目中第 k个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
#include<iostream>
using namespace std;
const int N=1e6+10;
// head存储链表头(表示头节点的下标),e[i]存储节点i的值,ne[i]存储节点i的next指针,idx表示当前用到了哪个节点(相当于指针)
int head, e[N], ne[N], idx;
// 初始化:链表是空的:head->空,空节点的下标用-1来表示
void init()
{
head = -1;
idx = 0;
}
//在表头插入一个数a
void add_to_head(int a)
{
e[idx] = a;
ne[idx] = head;//(1)插入的节点的next指针指向了原本head指向数据
head=idx;//(2)原本的head指向了插入元素a;
idx++;
}
//题目中第k个插入的数并不是指当前链表的第k个数。
//例如操作过程中一共插入了n个数,则按照插入的时间顺序,这n个数依次为:第1个插入的数,第2个插入的数,…第 n个插入的数。
//将数据x插入到下标是k的节点后面
void add_to_k(int k,int x)
{
e[idx]=x;
ne[idx]=ne[k];
ne[k]=idx;
idx++;
}
//将第k个的节点后面的一个节点删掉
void remove(int k)
{
ne[k]=ne[ne[k]];
}
int main()
{
int m;//操作次数
cin>>m;
init();//初始化
while(m--)
{
char s;
int k;
int x;
cin>>s;
if(s=='H')
{
cin>>x;
add_to_head(x);
}
else if(s=='I')
{
cin>>k>>x;
add_to_k(k-1,x);
}
else
{
cin>>k;
if(!k) head=ne[head];//当k为0时,表示删除头结点。
//举例:1,2,3,4。k=0时,删除1即可,即head指向2,2的下标为ne[head];
remove(k-1);
}
}
for(int i=head;i!=-1;i=ne[i])
{
cout<<e[i]<<' ';
}
cout<<endl;
return 0;
}
2.链表之静态链表(用数组模拟双链表)
例题6:双链表
#include<iostream>
using namespace std;
#include<string>
const int N=1e6+10;
//e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;
//struct Node{
//int e,l,r;
//}nodes[N];
// 初始化
void init()
{
//0是左端点(head),1是右端点(tail)
r[0] = 1, l[1] = 0;
idx = 2;//因为0和1已经被占用了
}
//表示在第 k 个插入的数右侧插入一个数x。
void add(int k,int x)
{
e[idx]=x;
r[idx]=r[k];
l[idx]=k;
l[r[k]]=idx;//r[k]是第k个插入节点的右边节点的idx
r[k]=idx;
idx++;
}
//在第 k 个插入的数左侧插入一个数x:其实就是在第l[k]个数右边插入一个数x
//add(l[k],x)
//表示将第 k 个插入的数删除,第 k 个数左边的数的下标l[k],第 k 个数右边的数的下标r[k]
void remove(int k)
{
r[l[k]]=r[k];
l[r[k]]=l[k];
//nodes[nodes[k].l].r=nodes[k].r;
}
int main()
{
int m;//操作次数
cin>>m;
init();
while(m--)
{
string s;
cin>>s;
int k;
int x;
if(s=="L")//表示在链表的最左端插入数x,即在head的右边插入x
{
cin>>x;
add(0,x);
}
else if(s=="R")//表示在链表的最右端插入数x,即在tail的左边插入x
{
cin>>x;
add(l[1],x);
}
else if(s=="D")
{
cin>>k;
remove(k+1);
}
else if(s=="IR")
{
cin>>k>>x;
add(k+1,x);
}
else
{
cin>>k>>x;
add(l[k+1],x);
}
}
for(int i=r[0];i!=1;i=r[i]) cout<<e[i]<<' ';
cout<<endl;
return 0;
}
3.链表之删除元素
例题7:移除链表元素:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
题目 难度:简单
在这道题里头结点(题目中也给出了定义:ListNode* head)的val是有正常数字的,不是摆设,所以我们可以设一个虚拟头结点放在原来头结点前面。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeElements(ListNode* head, int val)
{
ListNode* dummy_head=new ListNode(0);//设置虚拟头结点
dummy_head->next=head;//设置虚拟头节点;放在head前面
ListNode *cur=dummy_head;
while(cur->next!=nullptr)
{
if(cur->next->val==val)
{
ListNode *node_delete=cur->next;
ListNode *tmp=cur->next->next;
//cur是要删除结点的上一个结点的指针,cur->next->next是要删除结点的下一个结点指针
cur->next=tmp;
delete node_delete;
}