顺序表是常用的数据结构,一起来学习顺序表吧
目录
线性表
线性表是n个具有相同特性的数据元素的有限序列,是一种实际中广泛使用的数据结构,常见的线性表有顺序表、链表、栈、队列、字符串。
线性表在逻辑上是线性结构,即连续的一条直线,但在物理上不一定连续,线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表及其特点
数组缺陷:定义数组时必须指定数组大小,但是如果指定的大小不能满足使用空间需求时,就会有问题。
顺序表含义及特点:顺序表本质是数组,是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储,在数组上完成数据的增删查改,顺序表要从左往右连续存储数据。顺序表的逻辑结构和物理结构都是连续的。
2.顺序表分类:顺序表有两种,包括静态顺序表和动态顺序表
(1) 静态顺序表:使用定长数组存储元素。
//顺序表的静态存储
#define N 7
typedef int SLDataType;
typedef struct SeqList
{
SLDataType array[N];//定长数组
size_t size;//有效数据的个数
}SeqList;
(2)动态顺序表:使用动态开辟的数组存储。
typedef struct SeqList
{
SLDataType* array;//指向动态开辟的数组
size_t size;//有效数据的个数
size_t capacity;//容量
}SeqList;
3. 顺序表缺陷:
(1)动态增容有性能消耗。
(2)当头部插入数据时,需要挪动数据
顺序表的实现
静态顺序表容量是固定的,因此静态顺序表不实用。那么如何实现动态顺序表呢?
18-dynamicSequenceTable.h(结构体定义和方法声明)
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
//为int类型重新定义新名称
//如果后续想把SeqList中成员*a的类型改为float或char,不需要把每个使用*a的地方都修改一遍,直接修改typdef定义类型即可
typedef int SeqDataType;
typedef struct SeqList
{
SeqDataType* a;
int size;//有效数据的个数
int capacity;//容量
}SeqList,SEQ;
//内存中管理数据的结构增删改查的接口
//初始化
void SeqListInit(SeqList* seq);
//销毁
void SeqListDestroy(SeqList* seq);
//打印
void SeqListPrint(SeqList* seq);
//扩容
void SeqCheckCapacity(SeqList* seq);
//尾插
void SeqListPushBack(SeqList* seq, SeqDataType x);
//头插
void SeqListPushFront(SeqList* seq, SeqDataType x);
//尾删
void SeqListPopBack(SeqList* seq);
//头删
void SeqListPopFront(SeqList* seq);
//查找
int SeqListFind(SeqList* seq, SeqDataType x);
//某一位置插入数据
void SeqListInsert(SeqList* seq, int pos, SeqDataType x);
//某一位置删除数据
void SeqListErase(SeqList* seq, int pos);
//某一位置删除数据
void SeqListModify(SeqList* seq, int pos,SeqDataType x);
18-dynamicSequenceTable.c(方法实现)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include "18-dynamicSequenceTable.h"
//初始化
void SeqListInit(SeqList* pq)
{
assert(pq);
//初始化每个成员变量的值
pq->a = NULL;
pq->size = 0;
pq->capacity = 0;
}
//销毁
void SeqListDestroy(SeqList* pq)
{
assert(pq);
free(pq->a);
pq->a = NULL;
pq->size = 0;
pq->capacity = 0;
}
//扩容
void SeqCheckCapacity(SeqList* pq)
{
//判断是否需要扩容
if (pq->size == pq->capacity)
{
//如果capacity=0,就申请4个a的空间,如果不为0,就申请2倍大的capacity的空间
//一般扩容都扩容成原来2倍,太小则需要频繁扩容,太大则有可能浪费较多空间
int newCapacity = pq->capacity == 0 ? 4 : pq->capacity * 2;
SeqDataType* newA = (SeqDataType*)realloc(pq->a, sizeof(SeqDataType) * newCapacity);
if (newA == NULL)
{
printf("realloc fail\n");
return;
}
pq->a = newA;
pq->capacity = newCapacity;
}
}
//尾插
void SeqListPushBack(SeqList* pq, SeqDataType x)
{
assert(pq);
//判断是否需要扩容
SeqCheckCapacity(pq);
//把第size个元素值置为x
pq->a[pq->size] = x;
//size++
pq->size++;
}
//头插
void SeqListPushFront(SeqList* pq, SeqDataType x)
{
assert(pq);
//判断是否需要扩容
SeqCheckCapacity(pq);
//在第一个位置插入数据需要把所有元素向后挪动一个位置,要从最后一个元素开始依次把所有数据拷贝到下一个位置
int end = pq->size-1;
while (end>=0)
{
//将元素拷贝到该元素下一个位置
pq->a[end + 1] = pq->a[end];
end--;
}
//将x放在第一个位置
pq->a[0] = x;
//size++
pq->size++;
}
//打印
void SeqListPrint(SeqList* pq)
{
assert(pq);
int i = 0;
for (i = 0; i < pq->size; i++)
{
printf("%d ", pq->a[i]);
}
printf("\n");
}
//尾删
void SeqListPopBack(SeqList* pq)
{
assert(pq);
assert(pq->size>0);
//直接将元素个数-1即可,数据删除不删除无所谓
pq->size--;
}
//头删
void SeqListPopFront(SeqList* pq)
{
assert(pq);
assert(pq->size > 0);
int begin = 0;
while (begin < pq->size)
{
//删除第一个元素,需要从前往后将其余元素依次拷贝到数组中,如果从后往前拷贝,那么所有元素值都为最后一个元素值
pq->a[begin] = pq->a[begin + 1];
begin++;
}
//size-1
pq->size--;
}
//查找
int SeqListFind(SeqList* pq, SeqDataType x)
{
assert(pq);
int i = 0;
for (i = 0; i < pq->size; i++)
{
if (pq->a[i] = x)
{
return i;
}
}
return -1;
}
//某一位置插入数据
void SeqListInsert(SeqList* pq, int pos, SeqDataType x)
{
assert(pq);
assert(pos>0 && pos < pq->size);
//判断是否需要扩容
SeqCheckCapacity(pq);
//从后往前拷贝数据
int i = pq->size-1;
while (i >= pos)
{
pq->a[i + 1] = pq->a[i];
i--;
}
pq->a[pos] = x;
pq->size++;
}
//某一位置删除数据
void SeqListErase(SeqList* pq, int pos)
{
assert(pq);
assert(pos > 0 && pos < pq->size);
int i = pos;
while (i < pq->size)
{
pq->a[i] = pq->a[i + 1];
i++;
}
pq->size--;
}
//修改某一位置数据
void SeqListModify(SeqList* pq, int pos, SeqDataType x)
{
assert(pq);
assert(pos >= 0 && pos < pq->size);
pq->a[pos] = x;
}
头插需要从后向前拷贝的原因:如果从前向后对数组依次拷贝,先拷贝1,最后拷贝5,就会把所有数据都变成小标为0的元素,因此要从后向前拷贝,先拷贝5,最后拷贝1。
18-test.c(测试、方法调用)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include "18-dynamicSequenceTable.h"
TestSEQList1()
{
SeqList s = {NULL,0,0};
//想要修改结构体变量,必须传结构体的地址
//因此SeqListInit的实参是结构体地址,形参应该是指针,用来接收结构体地址,其他方法同理
SeqListInit(&s);
//尾插 1 2 3 4 5
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPushBack(&s, 5);
//头插 0 0 0 0 0
SeqListPushFront(&s, 0);
SeqListPushFront(&s, 0);
SeqListPushFront(&s, 0);
SeqListPushFront(&s, 0);
SeqListPushFront(&s, 0);
SeqListPrint(&s);
//头删一个元素
SeqListPopFront(&s);
SeqListPrint(&s);
//尾删一个元素
SeqListPopBack(&s);
SeqListPrint(&s);
//指定位置插入一个元素
SeqListInsert(&s,2,6);
SeqListPrint(&s);
//指定位置删除一个元素
SeqListErase(&s, 2);
SeqListPrint(&s);
//修改指定位置元素
SeqListModify(&s, 0, -1);
SeqListPrint(&s);
//销毁顺序表
SeqListDestroy(&s);
}
int main()
{
TestSEQList1();
return 0;
}
执行结果:
顺序表的应用
1.力扣网- 数组形式的整数加法
对于非负整数 X 而言,X 的数组形式是每位数字按从左到右的顺序形成的数组。例如,如果 X = 1231,那么其数组形式为 [1,2,3,1]。给定非负整数 X 的数组形式 A,返回整数 X+K 的数组形式。示例:
输入:A = [9,9,9,9,9,9,9,9,9,9], K = 1
输出:[1,0,0,0,0,0,0,0,0,0,0]
解释:9999999999 + 1 = 10000000000
分析:
(1)不确定数组和数字相加的结果会不会产生进位,因此要用malloc为新数组分配空间,大小为数组和数字较大位数值+1。
(2)进位如何解决?将数组和数字对应位及进位相加的和如果比10大,那么该位结果就要-10,进位置1。
(3)对于(2)中的结果如何存放?如果将结果在新数组中从最右侧往最左侧存放,那么有可能相加的最终结果最高位没有产生进位,这就会导致新数组最高位为0,还需要将其他位顺次向前挪,如下图左图所示。因此应从最左侧往最右侧存放,记录新数组实际使用的长度,计算完毕再将数组逆序。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
int* addToArrayForm(int* A, int ASize, int k, int* returnSize)
{
int kSize = 0;
int num = k;
//计算数字k的位数
while (num)
{
num /= 10;
kSize++;
}
//新数组大小为:max{数组长度,数字位数}+1,+1的原因是并不知道相加的最高位会不会有进位
int len = ASize > kSize ? ASize + 1 : kSize + 1;
//为新数组malloc动态开辟空间
int* retArr = (int*)malloc(sizeof(int) * len);
if (retArr == NULL)
{
return NULL;
}
int Ai = ASize - 1;//数组下标
int Ki = 0;//数字从低到高第i位
int next = 0;//进位
int reti = 0;//新数组最终长度
//只要数组和数字有一个没走完就进while循环
while (Ai >= 0 || Ki < kSize)
{
int aval = 0;
if (Ai >= 0)
{
aval = A[Ai--];//取数组最右端一位
}
int kval = k % 10;//取数字最低位
k /= 10;
Ki++;
//相加和
int ret = aval + kval + next;
//判断相加和是否产生进位
if (ret >= 10)
{
next = 1;
ret -= 10;
}
else
{
next = 0;
}
//注意此时将结果在新数组中按照从左到右的顺序存储,因此执行结束后,新数组需要逆置
retArr[reti++] = ret;
}
//最高位相加的结果如果进位是1,那么还需将1存储在新数组最高位
if (next == 1)
{
retArr[reti++] = 1;
}
//逆置新数组
int begin = 0, end = reti - 1;
while (begin < end)
{
int temp = retArr[begin];
retArr[begin] = retArr[end];
retArr[end] = temp;
begin++;
end--;
}
*returnSize = reti;
return retArr;
}
int main()
{
int array[] = { 9,9,9,9,9 };
int len = sizeof(array) / sizeof(array[0]);
int* p = array;
int number = 999;
int newlen = 0;
int *p1 = addToArrayForm(array, len, number, &newlen);
int i = 0;
for (i = 0; i < newlen; i++)
{
printf("%d ", *(p1 + i));
}
return 0;
}
执行结果如下:
2.力扣网- 合并两个有序数组
给你两个按非递减顺序排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。请你合并 nums2 到 nums1 中,使合并后的数组同样按非递减顺序排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
分析:
(1)nums1长度为m+n,无需开辟新数组。
(2)从数组左侧开始比较还是从数组右侧开始比较?如果从左侧开始比较,如果nums2的数据值<nums1的数据值,那么nums2的数据值放在nums1的对应位上,nums1的数据就会被覆盖,找不回来了,如下图所示。因此要从数组右侧向左开始比较。
(3)什么时候比较结束?nums1和nums2有一个走完就不用比较了,因此进入循环的判断条件是nums1和nums2的下标同时要>=0,当有一个不满足条件时,表明有一个数组走完了。此时如果是nums1走完了,那么就要把nums1剩下的数据直接拷贝到nums1前面剩下的几个位置中;如果是nums2走完了,那么就不用动,因为这就说明nums1剩下的没走的那几个数据比nums2的数据都小。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
int end1 = m - 1;
int end2 = n - 1;
int end = m + n - 1;
//从右侧开始比较nums1和nums2的数据值,将较大者放在nums1数组的最右侧
while (end1 >= 0 && end2 >= 0)
{
if (nums1[end1] > nums2[end2])
{
nums1[end--] = nums1[end1--];
}
else
{
nums1[end--] = nums2[end2--];
}
}
//有一个数组走完,如果是nums2没走完,需要将nums2剩余数据拷贝到nums1前面位置中
//如果是nums1没走完,不用动,因为nums1剩下的数据是最小的,本身就在nums1中
while (end2 >= 0)
{
nums1[end--] = nums2[end2--];
}
}
int main()
{
int arr1[] = { 2,3,6,0,0,0,0};
int len1 = sizeof(arr1) / sizeof(arr1[0]);
int arr2[] = {1,2,5,7};
int len2 = sizeof(arr2) / sizeof(arr2[0]);
merge(arr1, len1, 3, arr2, len2, len2);
int i = 0;
for (i = 0; i < len1; i++)
{
printf("%d ", arr1[i]);
}
}
执行结果:
3力扣网-轮转数组
给你一个数组,将数组中的元素向右轮转 k
个位置,其中 k
是非负数。要求空间复杂度O(1)
示例:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
分析:有2种解法可以让空间复杂度为O(1):
(1)先实现旋转一次,再对旋转一次的方法执行旋转k次
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void rotate(int* nums, int numsSize,int k)
{
int i = 0;
int end = numsSize;
//旋转次数>数组长度,旋转次数需要对数组长度取模
k %= numsSize;
for (i = 0; i < k; i++)
{
//将最后一个元素保存下来
int temp = nums[numsSize - 1];
int j = 0;
//一次向右旋转,将其余元素依次向后挪动
for (j = numsSize - 2; j >= 0; j--)
{
nums[j + 1] = nums[j];
}
//把最后一个元素放在数组首元素的位置
nums[0] = temp;
}
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7 };
int len = sizeof(arr) / sizeof(arr[0]);
int k = 6;
rotate(arr, len, k);
int i = 0;
for (i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
执行结果:
(2)三步翻转法:将前k个元素进行翻转,再将后面len-k个元素进行翻转,最后整体翻转。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//实现从起始位置到结束位置的翻转
void reverse(int* nums, int begin, int end)
{
//起始位置与结束位置逐一对调
while (begin < end)
{
int temp = nums[begin];
nums[begin] = nums[end];
nums[end] = temp;
begin++;
end--;
}
}
void rotate(int* nums, int numsSize, int k)
{
reverse(nums, 0, k - 1);//翻转前k个元素
reverse(nums, k, numsSize - 1);//翻转后len-k个元素
reverse(nums, 0, numsSize - 1);//整体翻转
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7 };
int len = sizeof(arr) / sizeof(arr[0]);
int k = 3;
rotate(arr, len, k);
int i = 0;
for (i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
执行结果: