线性表
线性表示n个具有相同特性的数据元素的有限序列,线性表是一种在实际中广泛使用的数据结构,常见的线性表有:顺序表,链表,栈,队列,字符串等等。
线性表在逻辑上是线性结构的,也就是说是连续的一条直线。但在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表概念
顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元一次存储线性表中的各个元素、使得线性表中的逻辑结构上相邻的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上相邻的关系,采用顺序表是将表中的结点依次存放在计算机内存中一组地址连续的存储单元
顺序表和链表优点和缺点
顺序表
优点:可以根据便宜实现快速的随机读。
缺点:扩容,增删元素的时间复杂度高。
链表
优点:扩容方便,增删效率高。
缺点:不支持随机访问。
顺序表的操作
顺序表分为静态表和动态表
#define N 100
typedef int SLDataType;
//静态顺序表
struct seqList1
{
SLDataType data[N];
int size;
};
//动态顺序表 (常用)
typedef struct seqList
{
SLDataType* data;
int size;
int capacity;
}seqList;
因为动态顺序表是我们比较常用的,所以这里我主要讲的是对动态顺序表的一些操作(增删查改)。
操作接口
//初始化顺序表
void initseqList(seqList* sl);
//尾插:给顺序表最后一个有效数据的末尾插入新的数据
void seqListpushBack(seqList* sl, SLDataType val);
//尾删:删除最后一个数据
void seqListpopBack(seqList* sl);
//查找当前下标的元素
SLDataType seqListAt(seqList* sl, int pop);
//判断表是否为空
int seqListEmpty(seqList* sl);
//打印表
void seqListPrint(seqList* sl);
//查看表的有效个数
int seqListSize(seqList* sl);
//检查容量
void seqListCheckCapacity(seqList* sl);
//头插
void seqListPushFront(seqList* sl, SLDataType val);
//头删
void seqListPopFront(seqList* sl);
//任意插入
void seqListInsert(seqList* sl, int pos, SLDataType val);
//任意删除
void seqListErase(seqList* sl, int pos);
//查找返回索引
int seqListFind(seqList* sl, SLDataType val);
//删除表中的一个节点
void listErase(struct list* lst, struct listNode* node);
//销毁
void seqListDestroy(seqList* sl);
实现接口操作
#include"seqList.h"
//初始化顺序表
void initseqList(seqList* sl)
{
if (sl == NULL)
return;
sl->data = NULL;
sl->size = 0;
sl->capacity = 0;
}
void seqListpopBack(seqList* sl)
{
/*if (sl == NULL)
return;
sl->size--;*/
seqListErase(sl, sl->size-1);
}
int seqListEmpty(seqList* sl)
{
if (sl == NULL || sl->size == 0)
return 0;
return 1;
}
void seqListPrint(seqList* sl)
{
if (sl == NULL)
return ;
for (int i = 0; i < sl->size; i++)
{
printf("%d ", sl->data[i]);
}
printf("\n");
}
int seqListSize(seqList* sl)
{
if (sl == NULL || sl->size == 0)
return 0;
else
return sl->size;
}
void seqListpushBack(seqList* sl, SLDataType val)
{
//if (sl == NULL)
// return;
检查容量
//seqListCheckCapacity(sl);
//sl ->data[sl->size] = val;
//sl->size++;
seqListInsert(sl, sl->size, val);
}
SLDataType seqListAt(seqList* sl, int pop)
{
return sl->data[pop];
}
void seqListCheckCapacity(seqList* sl)
{
if (sl->size == sl->capacity)
{
//空间已满
//开新的空间
int newCapacity = sl->capacity == 0 ? 1 : 2 * sl->capacity;
sl->data = (SLDataType*)realloc(sl->data, newCapacity * sizeof(SLDataType));
if (sl->data)
return;
//更新容量
sl->capacity = newCapacity;
}
}
void seqListPushFront(seqList* sl, SLDataType val)
{
/*if (sl == NULL)
return;
seqListCheckCapacity(sl);
int end = sl->size;
while (end>0)
{
sl->data[end] = sl->data[end - 1];
--end;
}
sl->data[0] = val;
sl->size++;*/
seqListInsert(sl, 0, val);
}
void seqListPopFront(seqList* sl)
{
/*if (sl == NULL || sl->size == 0)
return;
int start = 0;
while (start<(sl->size))
{
sl->data[start] = sl->data[start+1];
++start;
}
sl->size--;*/
seqListErase(sl, 0);
}
void seqListInsert(seqList* sl, int pos, SLDataType val)
{
if (sl == NULL)
return;
if (pos <0|| pos>(sl->size))
return;
seqListCheckCapacity(sl);
int end = sl->size;
while (end > pos)
{
sl->data[end] = sl->data[end - 1];
--end;
}
sl->data[pos] = val;
sl->size++;
}
void seqListErase(seqList* sl, int pos)
{
if (sl == NULL || sl->size == 0)
return;
if (pos <0 || pos>=(sl->size))
return;
seqListCheckCapacity(sl);
int start = pos;
while (start < (sl->size)-1)
{
sl->data[start] = sl->data[start + 1];
++start;
}
sl->size--;
}
int seqListFind(seqList* sl, SLDataType val)
{
if (sl == NULL || sl->size == 0)
return -1;
for (int i = 0; i < sl->size; i++)
{
if (sl->data[i] == val)
return i;
}
return -1;
}
void seqListDestroy(seqList* sl)
{
if (sl != NULL && sl->data != NULL)
{
free(sl->data);
sl->data=NULL;
}
}
由上面的操作来看,我们可以看出顺序表的种种缺点
1、中间/头部的插入删除时间复杂度都为O(n)
2、增容需要申请新的空间,拷贝数据,释放旧的空间,都会有不小的消耗
3、增容一般是以2倍的增长,会造成空间的浪费
顺序表的应用举例
我们已经学会顺序表的一些基本操作,但是这对我们来说是太简单了,在笔试或者面试中一般都不会这么直接得考察你,而是在这些操作的基础上去完成一些更有价值的操作。例如:1、删除数组中所有值为val的节点,并且有时间O(n)空间O(1)的要求。2、删除排序数组的重复项。3、合并两个有序数组。4、旋转数组。5、数组形式做加法等等
1、删除数组中所有值为val的节点,并且有时间O(n)空间O(1)的要求
question1:给你一个数组 nums 和一个值 val,你需要原 移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
int removeElement(int* nums, int numsSize, int val)
{
int idx = 0;
for(int i=0;i<numsSize;i++)
{
if(nums[i]!=val)
{
nums[idx++]=nums[i];
}
}
return idx; //有效个数
}
2、删除排序数组的重复项
question2:给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
int removeDuplicates(int* nums, int numsSize){
int i = 0; //i数组的下标
if(nums == NULL || numsSize == 0) //如果是个空数组,则就没必要继续往下执行
return 0;
for(int j = 1; j < numsSize; j++)
{
if(nums[j] != nums[i]) //只要循环的数据不等于最后一个有效数字,都将它们存进数组里面
{
i++; //下标加一
nums[i] = nums[j]; //最后一个有效数组等于新来的数字
}
}
return i + 1;
}
3、合并两个有序数组
question3:给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。说明:初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。输入:nums1 = [1,2,3,0,0,0], m = 3 nums2 = [2,5,6], n = 3 输出:[1,2,2,3,5,6]
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){
int i=m-1; //i为第一个数组最后一个元素的下标
int j=n-1; //j为第二个数组最后一个元素的下标
int idx=m+n-1; //两数组合并后最后一个元素的下标
while(i>=0 && j>=0)
{
if(nums1[i]>=nums2[j]) //如果第一个数组的元素比第二个数组的元素大
{
nums1[idx--]=nums1[i--]; //那就将该元素排在合并后的数组的最后一个位置
}
else{
nums1[idx--]=nums2[j--];
}
}
//如果nums2中有剩余元素,拷贝
if(j>=0)
{
//将第二个数组中的剩余元素放到合并数组元素的头端,因为比较后剩下的元素肯定都比比过的元素小
memcpy(nums1,nums2,sizeof(int)*(j+1));
}
}
4、旋转数组
question4:给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。输入: [1,2,3,4,5,6,7] 和 k = 3 输出: [5,6,7,1,2,3,4]
void reverse(int *nums,int start,int end) //交换函数
{
while(start<end)
{
nums[start]=nums[start]^nums[end];
nums[end]=nums[start]^nums[end];
nums[start]=nums[start]^nums[end];
++start;
--end;
}
}
void rotate(int* nums, int numsSize, int k){
k%=numsSize; //当k>数组长度时,相当于反转k对数组长度取余的个数
reverse(nums,0,numsSize-k-1);
reverse(nums,numsSize-k,numsSize-1);
reverse(nums,0,numsSize-1);
}
5、数组形式做加法
question5:对于非负整数 X 而言,X 的数组形式是每位数字按从左到右的顺序形成的数组。例如,如果 X = 1231,那么其数组形式为 [1,2,3,1]。给定非负整数 X 的数组形式 A,返回整数 X+K 的数组形式。输入:A = [1,2,0,0], K = 34输出:[1,2,3,4]解释:1200 + 34 = 1234
int* addToArrayForm(int* A, int ASize, int k, int* returnSize){
int len =0; //k的长度
int temp = k;
while(temp)
{
len++;
temp/=10;
}
int arrlen = ASize>len ? ASize+1:len+1; //长度为最大数的长度+1
int *arr = (int*)malloc(sizeof(int)*(arrlen+1));
int end = ASize-1; //数组从最后一个元素开始相加
int step = 0; //进位值
int sum = 0; //相加结果
int idx = 0; //相加后存放的数组下标
while(end>=0 || k>0) //只要有一个数还没算完,都要进行相加
{
sum = step; //先让相加的结果为进位值
if(end>=0)
sum += A[end];
if(k>0)
sum += k%10;
if(sum>9) //相加结果是两位数,就进位 10~18
{
step=1;
sum-=10;
}
else
{
step =0;
}
arr[idx++] = sum; //该位置为个位或者十位或者百位或者....相加的结果
end--; //数组向前一位
k/=10; //数字向更高的一位走
}
if(step == 1) //如果最后相加完后有进位,就让最后一个元素为1
arr[idx++]=1;
int start = 0;
int end1 = idx-1;
//反转数组
while(end1>start)
{
arr[start] = arr[start]^arr[end1];
arr[end1] = arr[start]^arr[end1];
arr[start] = arr[start]^arr[end1];
end1--;
start++;
}
*returnSize = idx;
return arr;
}
以上顺序表的基本操作还有关于数组的题都会做了,在笔试面试中遇到的问题都可以迎刃而解了。