前言
编程语言,相当于各个门派的武功;数据结构,则相当于内功。习得一招一式,称霸武林,内功修炼必不可少!
这篇文章系本人原创,谢绝转载。
以下将介绍数据结构中最基础也是最重要的一种结构——线性表。
线性表是最基本的数据结构,有两种存储方式:顺序结构、链表结构
注意:同一个线性表中的元素类型必须相同。
一、顺序结构
顺序表中,相邻元素之间的物理地址是连续的,也就是说,计算机会用一组连续的内存单元存放表中各个元素。从下面的代码中可以看出数组cs的指针地址指向第一个元素。
char cs[] = {'a','b','c'};
printf("cs = %p \n",cs);//打印数组地址
for (int i = 0; i < 3; i++) {
printf("cs[%d] = %p\n", i, &cs[i]);//打印元素地址
}
/*
打印结果
cs = 0x7ffee82dc08d
cs[0] = 0x7ffee82dc08d
cs[1] = 0x7ffee82dc08e
cs[2] = 0x7ffee82dc08f
*/
下面是一道经典的数组指针的面试题
int a[5] = { 1, 2, 3, 4, 5 };
int *p = (int *)(&a + 1);
printf("%d,%d \n", *(a + 1), *(p - 1));
//打印结果:2,5
由于数组中的元素的地址是连续的,又因为int型占4个字节。*是指针运算符,&是取址运算符,它俩是互为相反的。具体可以看*运算符与&运算符
分析步骤
1.打印数组中元素地址
for (int i=0; i < 5; i++) {
printf("a[%d] = %p\n", i, &a[i]);
}
/*
a[0] = 0x7ffee79d3080
a[1] = 0x7ffee79d3084
a[2] = 0x7ffee79d3088
a[3] = 0x7ffee79d308c
a[4] = 0x7ffee79d3090
*/
2.&a取出a的地址,也就是第一个元素的地址,然后+1,则p指向a数组最后一个元素的后面一个存储空间,打印出p的指针为p = 0x7ffee79d3094,然后p-1,则相当于向前移动4个字节,所以指向的是数组a中最后一个元素,结果为5。a是数组名,也是首元素的地址,然后+1,则相当于后移动4个字节,所以指向第二个元素,结果为2。
1.插入删除操作时,效率比较低
这是因为在插入、删除时,需要移动大量的数据元素,比较耗时。假设顺序表中元素个数为n(不考虑顺序表扩容问题),将新元素插入最后一个位置,则时间复杂度为O(1);插入新的元素到第一个位置,则需要将之后的n个元素整体向后移动,时间复杂度为O(n),平均的时间复杂度为O((n+1)/2),也就是平均需要移动一半的元素。删除元素也是同理。
2.存取元素值效率比较高
如果已知元素的下标,则可以直接取出或修改元素的值,时间复杂度为O(1)。如果已知元素值为x,查找出元素的下标,则需要从第一个元开始分别与x比较,如果该元素刚好在第一个元素,则时间复杂度为O(1),如果在最后一个元素则为O(n),平均为O((n+1)/2)。
总结:顺序表在插入删除时,需要大量移动数据元素,效率较低。由于是静态存储结构,需要确定数组的大小,容量有限。适用于频繁存取元素数据。
具体实现如下:
#define MAXSIZE 10
typedef struct {
int data[MAXSIZE];
int last;//记录数组中最后一个元素的位置,默认值为-1,表是空表
}SeqList;
//指定位置插入元素
int insert_seqList(SeqList *list,int i,int x){
if(list->last == MAXSIZE-1){
return -1;//表空间已满,不能插入
}
if(i < 0 || i > list->last+1){
return -1;//插入位置不正确
}else{
//将i之后的元素整体向后移动
for (int j = list->last; j>=i; j--) {
list->data[j+1] = list->data[j];
}
list->data[i] = x;
list->last++;
}
return 1;
}
//删除指定位置的元素
void delete_seqList(SeqList *list,int i){
if(list->last < 0){
return;//空表
}
if(i<0 || i>list->last+1){
return;//删除的下标越界
}
//i之后的元素整体向前移动
for (int j=i; jlast; j++) {
list->data[j] = list->data[j+1];
}
list->last--;
}
//根据数值查找出在表中的下标
int location_seqlist(SeqList *list,int x){
if(list->last < 0){
return -1;//空表
}
for (int i=0; i<=list->last; i++) {
if(list->data[i] == x){
return i;
}
}
return -1;
}
//打印数组
void print_list(SeqList *list){
if(list->last < 0){
printf("数组为空 \n");
}else{
printf("--------------数据个数为 %d--------------\n",list->last+1);
for (int i=0; ilast+1; i++) {
printf("数组第%d个 = %d \n",i,list->data[i]);
}
}
}
int main(int argc, char * argv[]) {
NSLog(@"%s",__func__);
SeqList list;
//设置默认值
list.last = 2;
list.data[0] = 1;
list.data[1] = 2;
list.data[2] = 3;
//插入
insert_seqList(&list, 0, 11);
insert_seqList(&list, 0, 22);
insert_seqList(&list, 0, 33);
insert_seqList(&list, 0, 44);
insert_seqList(&list, 0, 55);
print_list(&list);
//删除
delete_seqList(&list, 0);
delete_seqList(&list, 5);
print_list(&list);
int i = location_seqlist(&list, 3);
printf("数据下标 i = %d \n",i);
}
二、链表结构
链表中元素的物理地址可以是不连续的。链表与顺序表不同,它是一种动态管理的存储结构,链表中的每个节点占用的内存空间不是预先分配的,而是运行时系统根据需求生成的。
每个节点包含元素数值外,还包含下一个节点的地址,如果是双向链表,还包含上一个节点的地址。
1.插入删除操作,效率较高
由于链表在插入删除时,不需要大量移动数据元素。又因为是动态存储结构,不需要预先定义大小,可根据需求申请或释放节点。如果在某个节点p之后插入新的节点s,则时间复杂度为O(1)。如果在某个节点p之前插入新的节点s,则需要从第一个节点开始遍历找到新节点s的直接前驱节点p,时间复杂度为O(n),有一种更好的实现方式,直接将新的节点s插入到节点p之后,然后交换节点p于节点s的数值,这样的操作时间复杂度为O(1)。
2.存取元素值效率比较低
不适用于直接存取操作,要取出表中任意一个元素必须从第一个元素开始向后查询。如果查找的节点是第一个节点,则时间复杂度为O(1);如果查找的节点是最后一个节点,则时间复杂度为O(n),平均操作时间复杂度为O((n+1)/2)。
1.单向链表
对单向链表而言,只能从头节点开始遍历整个链表。
单向链表的使用例子,计算两个多项式的和
//头文件
@interface WLPoly : NSObject
@property (nonatomic,assign) NSInteger coef;//系数
@property (nonatomic,assign) NSInteger exp;//指数
@property (nonatomic,strong) WLPoly *nextNode;
//两个多项式相加
+(void)addPoly:(WLPoly *)poly1 poly2:(WLPoly *)poly2;
//打印多项式
+(void)printPoly:(WLPoly *)poly;
@end
//实现
//多项式的相加
+(void)addPoly:(WLPoly *)poly1 poly2:(WLPoly *)poly2{
WLPoly *p = poly1.nextNode;//多项式1的相加指针变量
WLPoly *q = poly2.nextNode;//多项式2的相加指针变量
WLPoly *r = poly1;//
WLPoly *pc = poly1;//
while (p != nil && q != nil) {
//多项式的指数相等,系数相加
if(p.exp == q.exp){
NSInteger sum = p.coef + q.coef;
if(sum != 0){//系数和非零
p.coef = sum;
r = p;
}else{//系数和为零
r.nextNode = p.nextNode;
}
p = p.nextNode;
q = q.nextNode;
}else if(p.exp > q.exp){
r.nextNode = p;
r = p;
p = p.nextNode;
}else{
r.nextNode = q;
r = q;
q = q.nextNode;
}
}
if(p == nil){
r.nextNode = q;
}else{
r.nextNode = p;
}
[self printPoly:pc];
}
+(void)printPoly:(WLPoly *)poly{
if(poly == nil){
return;
}
NSMutableString *string = [[NSMutableString alloc]initWithCapacity:10];
WLPoly *p = poly.nextNode;
while (p != nil) {
NSString *s = [NSString stringWithFormat:@"%ldX%ld",p.coef,p.exp];
if(p.coef < 0){
s = [NSString stringWithFormat:@"%ldX%ld",-p.coef,p.exp];
if(string.length > 0){
[string appendString:@" - "];
}
}else{
if(string.length > 0){
[string appendString:@" + "];
}
}
if(p.exp == 0){
s= [NSString stringWithFormat:@"%ld",p.coef];
}
[string appendString:s];
p = p.nextNode;
}
NSLog(@"最后结果:%@",string);
}
测试代码
-(void)testPoly{
WLPoly *polyA = [[WLPoly alloc]init];
WLPoly *p1 = [self nextNodeWithCoef:6 exp:13];
WLPoly *p2 = [self nextNodeWithCoef:2 exp:10];
WLPoly *p3 = [self nextNodeWithCoef:-5 exp:4];
WLPoly *p4 = [self nextNodeWithCoef:14 exp:0];
polyA.nextNode = p1;
p1.nextNode = p2;
p2.nextNode = p3;
p3.nextNode = p4;
WLPoly *polyB = [[WLPoly alloc]init];
WLPoly *q1 = [self nextNodeWithCoef:5 exp:13];
WLPoly *q2 = [self nextNodeWithCoef:3 exp:11];
WLPoly *q3 = [self nextNodeWithCoef:8 exp:6];
WLPoly *q4 = [self nextNodeWithCoef:5 exp:4];
polyB.nextNode = q1;
q1.nextNode = q2;
q2.nextNode = q3;
q3.nextNode = q4;
[WLPoly printPoly:polyA];
[WLPoly printPoly:polyB];
//[WLPoly addPoly:polyA poly2:polyB];
}
2.双向链表
在单链表中,可以找到任意一个节点的后继节点,但是无法找出他的前趋节点,这是单链表的缺点。
在双向链表中,每个节点中,除了数据字段外,还包含了两个指针,一个直线该节点的前趋节点,一个指向该节点的后继节点。有两个好处:一个是可以从头和尾搜索某个节点。二是如果某一条链失效了,还可以利用另一条链修复整个链表。
(1)插入一个新的节点
设p是双向链表中的一个节点,pre代表前趋节点,next代表后继节点,s指向值为x的新的节点。操作步骤如下:
s->pre = p->next;//1
p->pre ->next = s;//2
s->next = p;//3
p->pre = s;//4
示意图如下
(2)删除一个节点
设p是双向链表中的一个节点,pre代表前趋节点,删除p节点,操作步骤如下:
p->pre->next = p->next;//1
p->next->pre = p->pre;//2
free(p);
示意图如下
3.循环链表
单循环链表是在单链表基础上,将表头指针放入链表尾部节点的指针域中,这就构成了单循环链表了。单循环链表可以从表中任意一个节点开始遍历整个链表。
在实际应用中,一般使用尾指针代替头指针来进行某种操作,比如,将两个单循环链表首尾相连。
设定单循环链表hr1、hr2,它们的尾节点分别为r1、r2,操作语句如下
r2->next = r1->next;//将第一个链表的尾节点的下一个节点指针接入到第二个链表的尾节点中
r1->next = hr2->next;//将第二个链表的头节点的下一个节点指针接入到第一个链表的尾节点中
free(hr2);//释放第二个链表
r1 = r2;//合并成一个单循环链表,只有一个尾节点
操作示意图如下
三、如何选择存储结构
通常基于以下三点考虑
1.存储空间
顺序表的存储空间,在运行前必须声明大小,也就是说,必须设定一个最大值,如果最大值设置过大,则会造成资源浪费;如果设置过小,则容易溢出(这里不考虑扩容)。如果需要实现扩容,则需要重新创建一个更大的数组,然后将原数组中的元素拷贝到新的数组中,开销比较大。
链表不需要考虑存储空间大小,但链表的存储密度较低(存储密度是指一个节点中数据元素所占的存储单元和整个节点所占存储单元之比)。显然链表的存储密度小于1。
2.运算
顺序表中,访问第i个元素的时间复杂度为O(1),而链表中访问第i个元素的时间复杂度为O((n+1)/2)。所以,频繁访问元素,则顺序表优于链表。从插入删除角度考虑,则链表优于顺序表。
3.运行环境
顺序表比较容易实现,任何高级语言中都有数组类型,链表的操作是基于指针,相对来说,前者更简单一点。