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

顺序表:本质上就是数组,但是,可以动态增长,并且要求顺序表里面存储的数据必须是从左往右连续的、
当使用数组时,可以在数组的任何位置进行存储数据,比如:
但是,使用顺序表时,就必须保持数据的连续存储,这里的连续是指,数据存储的位置必须是连续的,并不是数据的值是连续的,比如:
顺序表的逻辑结构和物理结构是一致的,都是线性连续的、
顺序表也有一定的缺陷,比如:
1、动态增容有性能消耗,因为动态开辟内存空间是在堆区上进行的,若要进行增容,如果在堆区上的该原空间后面有足够大的空间还好,但
是,如果后面的空间不够大的话,就需要从别处找一个足够大的空间,然后把数据拷贝过来,再释放原来的空间,这就会造成性能消耗、
2、动态增容时通常是直接增容至原来空间的2倍或者1.5倍,此时,若增容后的空间使用不完的话,就会造成资源浪费、
3、如果需要头部插入数据,就需要把原来的数据进行往后移动,挪动数据就需要遍历数组,会有一定的代价,而链表就能克服这种缺陷、
链表:
链表是不需要增容的,因为,它的物理结构并不要求是连续的,只需要把增加的数据链接到链表上就行了,则其空间按需索取,如果在头部插入
数据的话,不需要挪动数据,只需要定义一个节点,让该节点的指针域指向原来链表中首元素的地址即可,这就完成了头插的需求,所以链表在
这些方面相对于顺序表是具有一定的优势的,当然她也存在一定的劣势,后面会进行陈述、
链表的逻辑结构和物理结构是不一致的,其逻辑结构是线性连续的,但其物理结构不是线性连续的、
所以,顺序表和链表的逻辑结构都是线性连续的,顺序表的物理结构也是连续的,但是,链表的物理结构不是线性连续的、
链表只有通过遍历才能知道他有多少个数据,顺序表不需要前驱和后继,而链表需要前驱和后继,单链表只有后继,只有下一个,而双链表既有
前驱也有后继、
此处所讲的顺序表和链表都属于线性表,指的是两者的逻辑结构是线性连续的,后期的堆,也是一种数据结构,其物理结构上是一个数组,逻辑
结构上是二叉树,所以,物理结构和逻辑结构不一定是一致的,而给数据结构称谓的时候,一般是按照其逻辑结构来给的,逻辑结构即自己假想
出来的结构,写代码也是按照自己假想,即按照逻辑结构来写,数据结构本质上是用来存储数据的、
2、顺序表 :
2.1、概念及结构:


2.2、接口函数代码以文件形式进行呈现:
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、例题三:
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、例题五:
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--];
}
}