顺序表和链表1

目录

1、线性表:

2、顺序表 :

2.1、概念及结构: 

2.2、接口函数代码以文件形式进行呈现:

2.2.1、test.c源文件:

2.2.2、SeqList.c源文件:

2.2.3、SeqList.h头文件:

2.3、数组相关面试题:

2.3.1、例题一:

2.3.2、例题二:

扫描二维码关注公众号,回复: 14288700 查看本文章

2.3.3、例题三:

2.3.4、例题四:

2.3.5、例题五:

2.4、顺序表的问题及思考:

1、线性表:

数据结构分为:线性表和非线性表、

线性表 n 个具有 相同特性的数据元素 有限序列 ,线性表是一种在实际中广泛使用的 数据结构 常见的线性 表: 顺序表、链表 、栈、队列、字
符串...    其中, 顺序表和链表都属于线性表、
线性表 逻辑上是线性结构 ,也就说是 连续的 一条直线,但是在 物理(内存)结构 并不一定 是连续的,线性表在 物理上存储 时,通常以 数组和
链式结构 形式存储

顺序表:本质上就是数组,但是,可以动态增长,并且要求顺序表里面存储的数据必须是从左往右连续的、

当使用数组时,可以在数组的任何位置进行存储数据,比如:

 但是,使用顺序表时,就必须保持数据的连续存储,这里的连续是指,数据存储的位置必须是连续的,并不是数据的值是连续的,比如:

顺序表逻辑结构和物理结构是一致的,都是线性连续的、

顺序表也有一定的缺陷,比如:

1、动态增容有性能消耗,因为动态开辟内存空间是在堆区上进行的,若要进行增容,如果在堆区上的该原空间后面有足够大的空间还好,但

      是,如果后面的空间不够大的话,就需要从别处找一个足够大的空间,然后把数据拷贝过来,再释放原来的空间,这就会造成性能消耗、

2、动态增容时通常是直接增容至原来空间的2倍或者1.5倍,此时,若增容后的空间使用不完的话,就会造成资源浪费、

3、如果需要头部插入数据,就需要把原来的数据进行往后移动,挪动数据就需要遍历数组,会有一定的代价,而链表就能克服这种缺陷

链表:

链表是不需要增容的,因为,它的物理结构并不要求是连续的,只需要把增加的数据链接到链表上就行了,则其空间按需索取,如果在头部插入

数据的话,不需要挪动数据,只需要定义一个节点,让该节点的指针域指向原来链表中首元素的地址即可,这就完成了头插的需求,所以链表在

这些方面相对于顺序表是具有一定的优势的,当然她也存在一定的劣势,后面会进行陈述、

链表逻辑结构和物理结构是不一致的,其逻辑结构是线性连续的,但其物理结构不是线性连续的、

所以,顺序表和链表逻辑结构都是线性连续的,顺序表的物理结构也是连续的,但是,链表的物理结构不是线性连续的、

链表只有通过遍历才能知道他有多少个数据,顺序表不需要前驱和后继,而链表需要前驱和后继单链表只有后继,只有下一个,而双链表既有

前驱也有后继、

此处所讲的顺序表和链表都属于线性表,指的是两者的逻辑结构是线性连续的,后期的,也是一种数据结构,其物理结构上是一个数组逻辑

结构上是二叉树,所以,物理结构和逻辑结构不一定是一致的,而给数据结构称谓的时候,一般是按照其逻辑结构来给的,逻辑结构即自己假想

出来的结构,写代码也是按照自己假想,即按照逻辑结构来写,数据结构本质上是用来存储数据的、

2、顺序表

2.1、概念及结构: 

顺序表是用一段 物理地址连续 的存储单元依次存储数据元素的 线性结构 ,一般情况下采用 数组存储 ,在数组上完成数据的 增删查改
顺序表一般可以分为:
1、 静态顺序表 :使用 定长数组 存储元素:

2、 动态顺序表 :使用 动态开辟 的数组存储:

2.2、接口函数代码以文件形式进行呈现

静态顺序表 只适用于 确定知道需要存多少数据 的场景,静态顺序表的 定长数组 导致 N 定大了,空间开多了浪费,开少了不够用,所以现实中基本
都是使用 动态顺序表 ,根据需要动态的分配空间大小,所以下面我们实现 动态顺序表、

2.2.1、test.c源文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include"SeqList.h"

enum Option//枚举成员变量从0开始,依次递增1;
{
	//枚举中的变量一般都采用大写。
	EXIT,
	SEQLISTPUSHBACK,
	SEQLISTPUSHFRONT,
	SEQLISTPOPBACK,
	SEQLISTPOPFRONT,
	SEQLISTINSERT,
	SEQLISTERASE,
	SEQLISTPRINT,
	SEQLISTMODIFY,
	SEQLISTFIND,
};

void menu()
{
	printf("************************************************\n");
	printf("******  1、尾插数据     2、头插数据    *********\n");
	printf("******  3、尾删数据     4、头删数据    *********\n");
	printf("******  5、任意插数据   6、任意删数据  *********\n");
	printf("******  7、打印数据     8、修改数据    *********\n");
	printf("******  9、查找数据     0、退出        *********\n");
	printf("************************************************\n");
}

int main()
{
	int input= 0;
	SeqList s; //结构体变量,在这最好不进行初始化,后面通过调用函数来实现相应的功能、
	//初始化
	SeqListInit(&s);   //传址调用
	do
	{
		menu();
		printf("请选择对应的功能:>");
		scanf("%d", &input);
		int x = 0;
		int pos = 0;
		switch (input)
		{
			case SEQLISTPUSHBACK:
				//尾插
				printf("请输入数据,以-1结束:>");
				while (1)
				{
					scanf("%d", &x);
					if (x == -1)
					{
						break;
					}
					else
					{
						SeqListPushBack(&s, x);
					}
				}
				break;
			case SEQLISTPUSHFRONT:
				//头插
				printf("请输入数据,以-1结束:>");
				while (1)
				{
					scanf("%d", &x);
					if (x == -1)
					{
						break;
					}
					else
					{
						SeqListPushFront(&s, x);
					}
				}
				break;
			case SEQLISTPOPBACK:
				//尾删
				SeqListPopBack(&s);
				break;
			case SEQLISTPOPFRONT:
				//头删
				SeqListPopFront(&s);
				break;
			case SEQLISTINSERT:
				//任意位置插
				printf("请输入要插入的位置:>");
				scanf("%d", &pos);
				printf("请输入要插入的数据:>");
				scanf("%d", &x);
				SeqListInsert(&s, pos, x);
				break;
			case SEQLISTERASE:
				//任意位置删
				printf("请输入要删除的数据的位置:>");
				scanf("%d", &pos);
				SeqListErase(&s, pos);
				break;
			case SEQLISTPRINT:
				//打印
				SeqListPrint(&s);
				break;
			case SEQLISTMODIFY:
				//修改
				printf("请输入要修改的位置:>");
				scanf("%d", &pos);
				printf("请输入修改后的数据:>");
				scanf("%d", &x);
				SeqListModify(&s, pos, x);
				break;
			case SEQLISTFIND:
				//查找
				printf("请输入要查找的数据:>");
				scanf("%d", &x);
				SeqListFind(&s, x);
				int ret = SeqListFind(&s, x);
				if (ret == -1)
				{
					printf("没找到该数据\n");
				}
				else
				{
					printf("找到了,其下标为:%d\n", ret);
				}
				break;
			case EXIT:
				//销毁(释放)
				SeqListDestroy(&s);
				printf("空间释放完毕,退出成功\n");
				break;
			default:
				printf("选择错误,请重新进行选择\n");
				break;
		}
	} while (input);
	return 0;
}

2.2.2、SeqList.c源文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include"SeqList.h"

//初始化
void SeqListInit(SeqList* psl)
{
	assert(psl);
	psl->a = NULL;
	psl->size = 0;
	psl->capacity = 0;
}

//检查是否需要增容,若需要则增容、
void SeqListCheckCapacity(SeqList* psl)
{
	//判断是否需要增容、
	if (psl->size == psl->capacity)
	{
		//需要增容
		//realloc函数动态增容时 通常 是直接增容至原来空间的2倍或者1.5倍,扩容是有代价的,不建议频繁进行扩容、
		size_t newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
		SLDataType* tmp = (SLDataType*)realloc(psl->a, sizeof(SLDataType)* newCapacity);
		if (tmp == NULL)
		{
			//由于 psl->a 初始化的结果是NULL,再使用realloc函数的话,相当于是动态开辟内存空间,而不是增容、
			//增容失败
			printf("realloc fail\n");
			return;
		}
		else
		{
			//增容成功
			psl->a = tmp;
			psl->capacity = newCapacity;
		}
	}
	//return;   如果调用函数不需要返回,则在调用函数内部最后一行可以加上return,也可以不用加、但是,如果调用函数需要返回值,则在调用函数内部最后一行上不写
	//return+返回值,是不行的,只写return也是不行的,必须写上return并且还要返回对应类型的数据才是可以的、
}

//尾插
void SeqListPushBack(SeqList* psl, SLDataType x)
{
	//assert(psl);
	检查是否需要增容,若需要则增容、
	//SeqListCheckCapacity(psl);//一级指针传参,一级指针接收、

	//	psl->a[psl->size] = x;
	//	psl->size++;

	SeqListInsert(psl, psl->size, x);

}

//尾删
//不管是头删还是尾删,一般不考虑空间的缩小,因为删除完之后,如果空间随着缩小的话,当再次添加的时候,有需要再开辟空间,比较麻烦、
void SeqListPopBack(SeqList* psl)
{
	/*assert(psl);
	if (psl->size > 0)
	{
		psl->size--;
	}*/

	SeqListErase(psl, psl->size-1);
}


//头插
//realloc函数只能往后扩容,不能在前面进行增容,若进行头插,则需要把顺序表中原有的数据往右移动,并且要从后往前移动才行、
void SeqListPushFront(SeqList* psl, SLDataType x)
{
	//assert(psl);
	//SeqListCheckCapacity(psl);//一级指针传参,一级指针接收、
	//int end = psl->size - 1;
	//while (end >= 0)
	//{
	//	psl->a[end + 1] = psl->a[end];
	//	end--;
	//}
	//psl->a[0] = x;
	//psl->size++;

	SeqListInsert(psl, 0, x);
}

//头删
//不管是头删还是尾删,一般不考虑空间的缩小,因为删除完之后,如果空间随着缩小的话,当再次添加的时候,有需要再开辟空间,比较麻烦、
void SeqListPopFront(SeqList* psl)
{
	//assert(psl);
	//assert(psl->size > 0);
	把首元素后面的元素依次向前移动一个位置,并且要从左往右进行移动、
	//int begin = 0;
	//while (begin < psl->size - 1)
	//{
	//	psl->a[begin] = psl->a[begin + 1];
	//	begin++;
	//}
	//psl->size--;

	SeqListErase(psl, 0);
}

//打印
void SeqListPrint(const SeqList* psl)
{
	assert(psl);
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		printf("%d ", psl->a[i]);
	}
	printf("\n");
}

//释放空间-销毁
//涉及到动态内存开辟,要对该空间进行释放、
void SeqListDestroy(SeqList* psl)
{
	assert(psl);
	free(psl->a);
	psl->a = NULL;
	psl->size = 0;
	psl->capacity = 0;
}

//查找
//若查找 到 则返回其下标,若找不到,则返回整型int数字 -1 ;
//如果存在多个x,则返回的是第一次出现x的位置的下标、
int SeqListFind(const SeqList* psl, SLDataType x)
{
	assert(psl);
	int i = 0;
	for (i = 0; i < psl->size; i++)
	{
		if (psl->a[i] == x)
		{
			return i;
		}
	}
	return -1;
}

//任意位置插
void SeqListInsert(SeqList* psl, int pos, SLDataType x)
{
	assert(psl);
	assert(pos >= 0 && pos <= psl->size);//任意位置插的时候要保证数据是连续存放的、
	//当在某一处插数值的时候,要先把该位置后面的所有的数据依次往后移动一个位置,并且要保证从右往左移动,再把要插入的数据放在空出来的位置上、
	SeqListCheckCapacity(psl);//一级指针传参,一级指针接收、
	int end = psl->size - 1;
	while (end>=pos)
	{
		psl->a[end + 1] = psl->a[end];
		end--;
	}
	psl->a[pos] = x;
	psl->size++;
}

//任意位置删
void SeqListErase(SeqList* psl, int pos)
{
	assert(psl);
	assert(pos >= 0 && pos < psl->size);
	assert(psl->size > 0);
	//任意位置删就要把该位置以后的数据全部依次向前移动一个位置,并且要从左往右移动才行、
	int begin = pos;
	while (begin < psl->size-1)
	{
		psl->a[begin] = psl->a[begin + 1];
		begin++;
	}
	psl->size--;
}

//修改
void SeqListModify(SeqList* psl, int pos, SLDataType x)
{
	assert(psl);
	assert(pos >= 0 && pos < psl->size);
	psl->a[pos] = x;
}

2.2.3、SeqList.h头文件:

//顺序表头文件、 

//顺序表要通过使用数组来实现、
#pragma once    //防止头文件被重复包含、
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

//#define N 100

//顺序表要求,存储的数据从下标为0的位置开始,连续存储、
//静态版本顺序表
//struct SeqList
//{
//	int a[N];  //定长数组、
//	int size;  //静态顺序表中有效数据的个数、
//};
//静态顺序表实用性太低,不推荐使用、

//动态版本顺序表
typedef int SLDataType;   //冒号不要忽略掉

//实现数据结构通常是管理内存中数据的结构从而实现增删查改的接口、
typedef struct SeqList
{
	SLDataType* a;  // 指向动态开辟的数组
	int size;       //记录有效数据的个数、
	int capacity;   //记录存储数据的最大容量、
}SL,SeqList;


//初始化
//void SeqListInit(SL* psl);
void SeqListInit(SeqList* psl);
//void SeqListInit(SeqList& psl);  此处的&不是取地址,是引用,这是C++的语法,同时把所有的源文件都改成cpp,就可以运行了、
//C++里面的引用,该语法是可以替代指针的、

//释放空间-销毁
//涉及到动态内存开辟,要对该空间进行释放、
void SeqListDestroy(SeqList* psl);

//尾插
void SeqListPushBack(SeqList* psl, SLDataType x);

//尾删
void SeqListPopBack(SeqList* psl);

//头插
void SeqListPushFront(SeqList* psl, SLDataType x);

//头删
void SeqListPopFront(SeqList* psl);

//打印
void SeqListPrint(const SeqList* psl);

//查找
//若查找 到 则返回其下标,若找不到,则返回整型int数字 -1 ;
int SeqListFind(const SeqList* psl,SLDataType x);

//任意位置插
void SeqListInsert(SeqList* psl, int pos, SLDataType x);

//任意位置删
void SeqListErase(SeqList* psl, int pos);

//修改
void SeqListModify(SeqList* psl, int pos, SLDataType x);

2.3、数组相关面试题:

2.3.1、例题一:

原地移除数组中所有的元素val,要求时间复杂度为O(N)空间复杂度为O(1)、   力扣

原地删除等价于空间复杂度为:O(1)、

思路一:

开辟一个新的数组,把原数组中不是val值的数据都放在新开辟的数组中去,但是由于原数组中元素个数是不确定的,并且原数组中不是val值的

数据的个数也是不确定的,但是最坏的情况就是,原数组中所有元素都不是val值,而原数组中元素的个数又是不确定的,所以则假设开辟的新的

数组元素个数为N个,或者可以理解为,开辟一个新的数组,把原数组中不是val值的数据都放在新开辟的数组中去,由于原数组中的元素的值每

次输入的都有可能不一样,所以,原数组中不是val值的数据的个数是不确定的,现在要把这些值都放在新开辟的数组中去, 所以新开辟的数组

的元素个数也是不确定的,所以假设新开辟的数组的元素个数为N个,又因为最后还要把新开辟好的数组中的元素拷贝到原数组中,这是因为题

目要求是在原数组中进行修改的,把原数组中不是val值的数组放在新开辟的数组中则需要遍历,之后还要把新数组中的数据拷贝到原数组中,又

因新开辟的数组中元素个数是不确定的,所以遍历新的数组时,默认新数组长度为N,所以,总的时间复杂度就是:O(N),,由于额外开辟了数

组,所以,空间复杂度是:O(N),不满足题目要求,不采用该方法、

思路二:

双指针,两个指针都指向原数组,刚开始时,两个指针都指向原数组首元素的地址,把原数组中不是val值的数据依次放在指针dst所指的位置

上,指针src指向的数据若不是val值,则把该值放在指针dst所指的位置上,同时,指针变量src和dst都各自++,若指针变量src指向的数据是val

值,则不把该值放在指针dst所指的位置上,此时,只需要指针变量src++即可,此时,时间复杂度是:O(N),遍历一遍即可,由于没有额外开辟

数组,是在原数组上进行操作的,所以,空间复杂度就是:O(1),满足题目要求、

int removeElement(int* nums, int numsSize, int val)
{
//字符串一般用指针,数组一般使用下标进行访问、

  /*  int src=0;
    int dst=0;
    int i=0;
    for(i=0;i<numsSize;i++)
    {
        if(nums[i]!=val)
        {
            nums[dst]=nums[src];
            dst++;
            src++;
        }
        else
        {
            src++;
        }
    }
    return dst;*/
    int src=0;
    int dst=0;
    while(src<numsSize)
    {
        if(nums[src]==val)
        {
            src++;
        }
        else
        {
            nums[dst]=nums[src];
            src++;
            dst++;
        }
    }
    return dst;
}

思路三:

对原数组进行第一次遍历,找到第一个val值,然后把该val值所在的位置后面的所有的数据依次由左向右的向前挪动一个位置,同时原数组中元

素个数减去一个,由于假设数组长度默认为N,所以,第一次遍历的时候执行次数为N次,然后再进行第二次遍历,由于原数组中元素的个数减

去1,所以,现在数组中元素个数为N-1个,再进行遍历的时候,执行次数为N-1次,,并且还不明确原数组中到底有几个val值,像这种不明确大

小的值,默认其为N个,当有1个val值时,需要遍历一次数组,执行次数为N次,当有两个val值的时候,第一次遍历执行次数为N次,要进行两次

遍历,第二次遍历执行次数为N-1次,同理,当有N个val值的时候,执行次数则为N-(N-1) === 1次,所以,总的执行次数为N+(N-1)+(N-

2)+..1,所以,时间复杂度则为:O(N^2),或者可以理解为,因为不知道原数组中,val值的个数为多少,就按最坏来看,即,原数组中全部的值

都是val,并且原数组长度默认为N,所以还是有N个val值,其次分析同上,所以时间复杂度是:O(N^2),,由于直接在原数组上进行操作,所以

没有开辟额外的空间,故,空间复杂度是:O(1)、

2.3.2、例题二:

旋转数组    力扣

思路一:

右旋K次,一次移动一个数字,定义一个变量tmp,把数组中最后一个数字保存在变量tmp内,默认数组长度为N,把数组中前N-1个值全部向右移

动一位,再把tmp中的值放在数组的首位,这就完成了一次右旋,右旋K次就可以达到目的,此时,每次右旋时,都要把数组中前N-1个值全部右

移一位,这就需要遍历数组,遍历一次数组执行的次数为N-1次,,现在,由于给定的K值是不定(未知)的,所以,假设K值也是N,所以,

乘积起来得到最终的时间复杂度是:O(N^2),由于没有额外开辟数组,所以,空间复杂度就是:O(1)

虽然该方法满足空间复杂度的要求,但是时间复杂度不太好,并且在力扣上不能正常运行,所以不采用该方法,

思路二:

额外开辟一个数组,把后K个数不改变顺序的依次放在新开辟的数组的前面,再把原数组中前N-K个数字直接拷贝到新数组内容的后面即可,此时

要开辟的新的数组的大小和原数组是一样大的,并且,原数组的大小默认为N,所以新开辟的数组的大小也是N,此时遍历一遍数组即可,则

间复杂度是:O(N),但是由于额外开辟了数组,所以,空间复杂度是:O(N),此时不满足要求,不采用该方法、

思路三:

三次逆置,先逆置前N-K个数字,再逆置后K个数字,然后整体再进行逆置,不需要额外开辟数组,所示空间复杂度是:O(1),,第一次遍历就可

以把前两部分逆置,第二次遍历逆置整体,执行次数为:2*N次,所以时间复杂度就是:O(N)、

void reverse(int* a,int left,int right)
{
    while(left<right)
    {
        int tmp=a[left];
        a[left]=a[right];
        a[right]=tmp;
        left++;
        right--;
    }
}
void rotate(int* nums, int numsSize, int k)
{
    k %=numsSize;
    reverse(nums,0,numsSize-k-1);
    reverse(nums,numsSize-k,numsSize-1);
    reverse(nums,0,numsSize-1);
}

2.3.3、例题三:

删除 有序数组 中的重复项、 力扣
思路一:
定义两个指针变量,分别指向原数组的首元素和第二个元素,由于是升序排列的,所以,相同的值一定是靠在一起的,如果两个指针所指向的值
不相等的话,则两个指针都进行++操作,因为若果两者不相等的话,前者一定是独一无二的,不需要处理,如果两个指针指向的内容是相等的,
则只需要让后面的指针进行++操作,直到后面得指针所指的内容与前面的指针所指的内容不相等为止,如果直接把后指针所指的内容包括其以后
的元素都向前挪动位置的话,就可以完成去重的操作,但是,这样做的话,指针所指的位置元素就发生了改变,不太好,所以要想到,额外开辟
新的数组,则,若两个指针所指的内容不相同的话,则把前指针所指的内容放到新数组中,然后两个指针分别++,若两指针所指内容相同的话,
则只对后指针进行++操作,直到两指针所指内容不相同为止,再把前指针所指内容放到新数组中去,之后再把后指针赋值给前指针,后指针再自
增一位,直到后指针遍历完整个数组即可,只有两个指针所指内容不同才将前指针所指内容放到新数组中,由于原数组的大小默认为N,且其中
只出现一次的元素个数是不确定的,最坏的是,原数组中所有的元素都只出现一次,所以,新开辟的数组额大小也设置成N,所以,额外开辟了
空间,则 空间复杂度为:O(N),,又因,需要通过遍历把原数组中只出现一次的数据放在新数组中,而原数组长度为N,其次,再需要遍历新数
组,把新数组中的数据拷贝到原数组中,因为要求在原数组中进行修改,而新数组长度也是N,所以, 时间复杂度是:O(N),不满足题目要求、
思路二:
采用 三指针的方式,其中, 两个指针都指向原数组的首元素,另外一个指针指向 原数组中第二个元素,其中分别是:cur,next,dst,cur和next
去判读所指内容是否相同,cur指向原数组首元素,next指向原数组第二个元素,dst用来记录去重后元素的个数,当cur和next所指内容不同时,
把cur所指内容赋值给dst所指内容,然后,cur++,next++,同时dst++,若cur和next所指内容相同的话,则只进行next++,直到naxt所指元素不
等于cur所指元素为止,再把cur所指内容赋给dst所指内容,同时,dst++,然后,cur=next,再让next++,直到next结束,,但是,此时,cur还
指向了一个值,然后判断该值是否要赋值给dst,这就完成了,此时,此时,没有额外开辟新的数组,所以, 空间复杂度就是:O(1),又因,只遍
历了一次原数组,所以, 时间复杂度就是:O(N),满足要求、
 
一般, 需要去重的话,都需要先进行 排序先排序后去重,如果 不排序直接去重的话,可以使用 C++的set < int > ,,其底层是 红黑树,后期再
讲、
int removeDuplicates(int* nums, int numsSize)
{
    //若数组中元素个数为0个,直接返回0即可、
    if(numsSize==0)
    {
        return 0; 
    }

    int cur=0;
    int next=1;
    int dst=0;
    while(next<numsSize)
    {
        if(nums[cur]!=nums[next])
        {
             nums[dst]=nums[cur];
             cur++;
             next++;
             dst++;
             //nums[dst++]=nums[cur++];
        }
        else
        {
            //防止非法访问,并且只能按照下面这个顺序来使用&&,因为在&&的使用中,如果前者为假,则后者就不再进行计算了,只有前者为真,才进行后者的计算,如果顺序颠倒,会先进行nums[cur]==nums[next]的运算,还是会出现非法访问的情况、
            //如果next出了数组,就默认其值是一个随机值,不会与数组中的数值相等,在VS下,越界访问可能查不出来,但是本质上是错误的,避免越界,但是在力扣上,越界能够查得出来,查出来就是错误,所以,在此必须加上条件防止越界、
           while( next<numsSize && nums[cur]==nums[next])
           {
               next++;
           }
            nums[dst]=nums[cur];
            cur=next;
            next++;
            dst++;
        }
    }

    if(cur<numsSize)
    {
        nums[dst++]=nums[cur];
    }
    return dst;
}   

2.3.4、例题四:

数组形式的 整数加法
/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* addToArrayForm(int* num, int numSize, int k, int* returnSize){



    //初始化k的位数为0位、
    int kSize=0;
    int nums=k;
    //计算K的位数、
    while(nums)
    {
        kSize++;
        nums /=10;
    }
    //计算两者相加后得到的数的最大位数,防止数组放不开、
    int len=numSize>kSize ? numSize+1:kSize+1;
    //像这种接口型题目需要动态开辟内存空间的情况,均默认能够开辟成功,在此不进行判断、
    int* retArr=(int*)malloc(sizeof(int)*len);
    int reti=0;

    //numi为数组最后一个元素的下标、
    int numi=numSize-1;
    //已知数字k的位数为kSize个,定义一个值ki,来记录运算过程中已经使用的数字k的位数为多少、
    int ki=0;//初始化为0,假设最开始对数字k的位数使用量为0、
    //只有当两者的个十百千..位都过一遍时才可以停止,若两者之中,有一个数的个十百千..位没有遍历完,都要继续等待另外一个数的所有位遍历完才可以结束,这是因为要考虑进位的情况,有的情况进位数一直是1,不遍历完的话会出错误,比如:[9 9 9 9 9 ]  k=1、 当数字k遍历完所有位时,此时进位数next恒等于1,就要一直不停地向前进位,所以只能等数组中所有位遍历完才知道结果,不可以直接把数组中没遍历的位直接拷贝下来、

    int next=0;//初始化进位数next为0、

    //因为ki从0开始,所以在这里只需要 ki < kSize 即可,不需要等于、
    while(numi>=0 || ki < kSize)
    {
        //numVal为数组中最后一个元素的值,即数组中该数值的个位,如果依次得到十位,百位....就需要通过下标进行访问,即每一次numi--即可,但是,如果存在数组短,数字k长的情况,当数组都遍历结束时,仍要进入while循环等待数字K遍历结束,但是,由于每一次进入while循环内部,numi就会自减1,,那么再使用num[numi]就会造成越界访问,所以在此不可直接对numi进行下标访问,则有:
        //int numVal=num[numi];
        //先判断,再通过下标访问
        int numVal=0;//初始化
        if(numi>=0)
        {
            numVal=num[numi--];
        }

        //依次取数字k的低位,此时不需要像上面一样判断,因为就算数组长,数字短,每一次自除得到的都是0,不影响结果,即使k已经等于0了,下一次0/10 或 0%10 仍等于0、
        int kVal=k%10;
        k /=10;
        ki++;

        //计算两者对应的位上的数字相加得到的数、
        int ret=numVal+kVal+next;
        if(ret >= 10)
        {
            next=1;
            ret -=10;
        }
        else
        {
            next=0; 
        }
        //由于多开辟了一个空间,先计算得到两者的个位,但如果把该数从数组的最后一个元素开始放置的话,如果多开辟的空间能使用上是最好的,如果使用不上的话,那么该开辟的数组首元素就会空出来,所以为了防止这种情况,我们把两者相加得到的个位上的数放在数组首元素的位置,后面的十位,百位,依次放置在数组第二第三位,后面再通过逆置的方法得到正常的顺序即可、
        retArr[reti++]=ret;
    }
    //如果数组中的数和数字k都遍历完了,即出了while循环之后进位数next仍等于1,,就要把该数再手动放置在数组中去、
    if(next==1)
    {
        retArr[reti++]=next;
    }

    //逆置数组中的数字、
    int begin =0;
    int end=reti-1;
    while(begin<end)
    {
        int tmp=retArr[begin];
        retArr[begin]=retArr[end];
        retArr[end]=tmp;
        begin++;
        end--;
    }
    //虽然开辟了len个空间,但是在此要给出确定使用的空间的个数,len只是最大的个数,可能使用了len个空间,也有可能没使用到len个空间,即使用了len-1个空间、
    //此处的reti的个数即为数组中有效元素的个数、
    *returnSize=reti;
    return retArr;
}

2.3.5、例题五:

合并两个有序数组 力扣
假设nums1的空间大小等于m+n,这样他就有足够的空间来保存来自nums2的元素、
思路一:
首先,从nums1和nums2两个数组的首元素开始比较,取较小值尾插到一个新的数组中,然后,把该较小值尾插在新的数组中后,再去访问该较
小值后面的元素,然后再进行比较,得出新的较小值尾插在新的数组中,再去访问该较小值后面的元素,再进行比较,如果恰巧两个元素相等,
就可以随便任选一个元素尾插在新的数组中去,一般来说如果下一次访问到的两个元素相等,那么就把上一次两个元素比较时,较小值所在的那
个数组中的此次比较相等的那个元素尾插在新的数组中,当两个数组中,若有其中一个数组,元素个数已经遍历结束,那么就把另外一个数组中
没有遍历的元素都拷贝到新数组中去,就得到了合并后的数组,由于此时的合并并不要求去重,所以,新开辟的空间个数应该是m+n个,但是由
于m和n都是未知(不确定)数,所以m+n也是一个不确定的数字,则假设其大小为N,所以, 空间复杂度就是:O(N),由于已知nums1数组的元素
个数为m个,nums2数组元素个数为n个,通过上述方法把两者合并到新的数组中,就需要对原数组nums1,nums2进行遍历,此过程执行次数
为:m+n,,此时,新数组中元素个数为m+n个,再把新数组中的数据拷贝到nums1中,执行次数也是m+n次,所以总的执行次数就是:
2*(m+n),所以 时间复杂度就是:O(m+n),这是因为,题目中并没有明确告诉m和n之间的关系,所以, 两者都不可以省略,都要保留下来、
思路二:
首先找到数组nums2中最大的数据,又因为是升序排列,即为数组nums2中最后面的数据,再找到数组nums1中有效元素的最后一个元素,也是
最大的数据,两者进行比较,把较大值放在数组nums1中最后一个位置,然后再访问刚才两个进行比较的元素的较大值的前一个元素,再和刚才
两个进行比较的元素的较小值进行比较,再把两者中新的较大值放在数组nums1中倒数第二个位置上,即,依次往前进行存储,,然后再访问上
一次比较中,较大值的前一个元素,再进行比较,如果元素相等,就任选一个依次倒着存放在nums1的位置上,直到数组nums1和nums2其中有
一个遍历结束为止,若是nums1先遍历结束,就把数组nums2中未进行遍历的元素一次倒序存放在数组nums1中,如果是数组nums2先遍历结
束,此时数组nums1就不需要再动其中的元素了,这是因为,把数组nums2往数组nums1中进行合并的原因,并且只能使用该方法去比较,如果
从两个数组最小值进行比较的话,由于合并后的数组也是升序排列,比较出来的较小值要从左往右存放,这时候就会造成数据覆盖,影响最后的
 
结果,此时,该方法只需要把两个数组中有效元素遍历一遍即可,所以 时间复杂度就是:O(m+n),,同时也没有开辟额外的内存空间,所以
间复杂度就是:O(1),满足要求、
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){

    //找到数组nums1中最后一个有效元素的下标、
    int end1=m-1;
    //寻找数组nums2中最有一个有效元素的下标、
    int end2=n-1;
    //寻找数组nums1中最后一个空间的下标、
    int end=m+n-1;
    //int end=nums1Size-1;
    //只有当两个数组中的有效元素都没遍历完才能进入循环继续、
    while(end1>=0 && end2>=0)
    {
        if(nums1[end1]>nums2[end2])
        {
            nums1[end--]=nums1[end1--];
        }
        else
        {
            //相等的话,随便哪个都可以、
            nums1[end--]=nums2[end2--];
        }
    }
    //两个数组其中一个数组中有效元素已经遍历完毕、
    //在循环中,不管执行哪一条语句,每一次执行要么end1--,要么end2--,所以,两者不可能同时到0,同理不可能同时到-1、
    //如果数组nums2中的有效元素先遍历结束,则整体不需要再附加条件,此时end2<0,不进入下面的while循环,就相当于是整体没有添加附加条件、
    //如果数组nums1中的有效元素先遍历结束,即end1<0,又因两者不可能同时结束,所以,此时数组nums2中的有效元素一定没有全部遍历结束,即end2不可能<0,要么等于0,要么大于0,进入循环就把剩下的没有遍历的元素拷贝到数组nums1中去、
    //由于在循环中,要么是end1--,要么是end2--,即两个数组中有效元素的个数不可能同时遍历结束,到此为止,要么是数组nums1中的有效元素先遍历结束,要么是数组nums2中的有效元素先遍历结束,若是数组nums2中的有效元素先遍历结束,那么数组nums1中的有效元素一定没有全部遍历结束,此时,即使没有全部遍历结束,也不需要改动数组nums1中现有的元素了,但是,如果数组nums1中的有效元素全部遍历结束,那么数组nums2中的有效元素一定不会全部遍历结束,此时就需要把nums2中的剩余的没有遍历的有效元素都拷贝到数组nums1中去、
        while(end2>=0)
        {
             nums1[end--]=nums2[end2--];
        }
}

2.4、顺序表的问题及思考:

问题:
1、 中间或头部的插入或删除 ,都需要 挪动数据 ,因为不管在中间还是在头部插入数据时,都需要先把后面的元素往后挪动一个位置之后,再把
      数据插进去,而不管在中间或者头部删除的时候,都要把后面的数据往前挪动一个位置来 保证连续存放 ,而挪动数据的时候就会涉及到遍
      历, 时间复杂度为O(N)、 所以在顺序表中尽量使用尾插或尾删,尽量不要使用其他位置的插入或删除、
2、 异地增容需要申请新空间,拷贝数据,释放旧空间,有不小的消耗,就算原地增容也会存在消耗,只是消耗相对较小、
3、 增容一般是呈 2 倍的增长,势必会有一定的 空间浪费 ,例如当前容量为 100 ,满了以后增容到 200, 我们再继续插入了5 个数据,后面没有数据
      插入了,那么就浪费了 95 个数据空间,如果 每次增容较少,会导致频繁扩容,效率低下 不可以按需申请或释放空间、
思考:
如何解决以上问题呢?下面给出了 链表 的结构来解决上述问题、
链表的内容将会在下一篇博客进行阐述,关于顺序表的基本知识已经梳理结束,希望大家点赞收藏加关注哦~

猜你喜欢

转载自blog.csdn.net/lcc11223/article/details/123353918
今日推荐