数据结构--顺序表与链表

1.顺序表特点与基本操作

1.1 顺序表定义

顺序表是一种线性表的存储结构,采用连续的内存空间存储数据元素。其逻辑顺序与物理顺序一致,通过数组实现,支持随机访问。顺序表属于静态存储结构,但可通过动态分配内存实现扩容。

1.2 顺序表特点

内存连续

所有元素存储在地址连续的存储单元中,通过首地址和下标可直接计算出元素位置,访问时间复杂度为 O(1)。

预先分配空间

需提前定义最大容量,静态分配可能导致空间浪费或不足。动态分配虽可扩容,但需重新申请内存并迁移数据,时间复杂度为O(n)。

插入删除效率低

在非尾部位置操作时,需移动后续元素以保持连续性。平均时间复杂度为 O(n),尾部操作则为 O(1)。

1.3 顺序表类型定义

cs 复制代码
// 定义顺序表结构体
typedef struct {
    int *elem;       // 指向存储元素的动态数组基地址
    int length;      // 当前顺序表中元素的实际个数
    int listsize;    // 当前顺序表分配的存储空间大小(最大能容纳的元素个数)
} sqlist;

1.4 初始化顺序表

cs 复制代码
/**
 * 初始化顺序表
 * @param L 待初始化的顺序表(引用传递,修改会影响实参)
 * @param n 初始要存入的元素个数
 * @return 1-初始化成功;0-内存分配失败
 */
int init(sqlist &L,int n) {
    // 为顺序表分配能存储100个int元素的初始空间
    L.elem = (int*)malloc(100 * sizeof(int));
    if (!L.elem) {  // 内存分配失败(返回NULL)
        return 0;
    }
    L.length = 0;   // 初始元素个数为0
    // 循环读取n个元素存入顺序表
    for (int i = 0; i < n; i++) {
        scanf("%d", &(L.elem[i]));  // 读取元素到数组对应位置
        L.length++;                 // 每存入一个元素,实际长度加1
    }
    L.listsize = 100;  // 记录当前分配的最大容量
    return 1;  

1.5 插入操作

cs 复制代码
/**
 * 向顺序表中插入元素
 * @param L 目标顺序表(引用传递)
 * @param i 插入位置(逻辑位置,从1开始计数)
 * @param e 要插入的元素值
 * @return 1-插入成功;0-位置不合法或内存扩容失败
 */

int insert(sqlist &L, int i, int e) {
    int *newspace;  // 用于扩容时指向新的内存空间
    int *p;         // 指向需要移动的元素
    int *q;         // 指向插入位置
    
    // 检查插入位置合法性:必须在1到当前长度+1之间(允许插在表尾)
    if (i < 1 || i > L.length + 1) {
        return 0;
    }
    
    // 若当前元素个数已达最大容量,需要扩容
    if (L.length >= L.listsize) {
        // 重新分配内存:在原有大小基础上增加10个元素的空间
        newspace = (int*)realloc(L.elem, (L.listsize + 10) * sizeof(int));
        if (!newspace) {  // 扩容失败
            return 0;
        }
        L.elem = newspace;  // 指向新的内存空间
        L.listsize += 10;   // 更新最大容量
    }
    
    q = &(L.elem[i - 1]);  // 计算插入的物理位置(逻辑位置i对应数组下标i-1)
    
    // 从最后一个元素开始,到插入位置为止,依次向后移动一个位置
    // (避免覆盖后续元素,必须从后往前移)
    for (p = &(L.elem[L.length - 1]); p >= q; --p) {
        *(p + 1) = *p;  // 后移元素
    }
    
    *q = e;      // 在插入位置存入新元素
    ++L.length;  // 实际长度加1
    return 1;    // 插入成功
}

图像解释与平均移动次数

1.6 删除操作

cs 复制代码
/**
 * 从顺序表中删除元素
 * @param L 目标顺序表(引用传递)
 * @param i 要删除的元素位置(逻辑位置,从1开始计数)
 * @param e 用于保存被删除的元素值(输出参数)
 * @return 1-删除成功;0-位置不合法
 */

int dele(sqlist &L, int i, int &e) {
    int *q;  // 指向顺序表最后一个元素
    int *p;  // 指向要删除的元素
    
    // 检查删除位置合法性:必须在1到当前长度之间
    if (i < 1 || i > L.length) {
        return 0;
    }
    
    p = &(L.elem[i - 1]);       // 找到要删除元素的物理位置(下标i-1)
    e = *p;                     // 保存被删除的元素值
    q = L.elem + L.length - 1;  // 指向最后一个元素(下标length-1)
    
    // 从删除位置的下一个元素开始,到最后一个元素为止,依次向前移动一个位置
    for (++p; p <= q; ++p) {
        *(p - 1) = *p;  // 前移元素,覆盖被删除的位置
    }
    
    --L.length;  // 实际长度减1
    return 1;  // 删除成功
}

算法时间主要花费在移动元素上

若删除尾节点,则根本无需移动,特别快。

若删除首节点,n-1个元素全部需要前移,特别慢。

1.7 查找元素位置

输入元素值,返回该元素值的位置

cs 复制代码
/**
 * 查找顺序表中指定元素的位置
 * @param L 目标顺序表(值传递,不修改原表)
 * @param e 要查找的元素值
 * @return 元素的逻辑位置(从1开始);0-未找到
 */

int locate(sqlist L, int e) {
    int i = 1;        // 记录逻辑位置(初始为第一个元素)
    int *p = L.elem;  // 指向第一个元素的指针
    
    // 循环遍历元素:未超出长度且未找到目标时继续
    while ((i <= L.length) && (*p != e)) {
        i++;  // 位置后移
        p++;  // 指针后移(指向下一个元素)
    }
    
    // 若找到(i未超出长度),返回逻辑位置;否则返回0
    if (i <= L.length) {
        return i;
    } else {
        return 0;
    }
}

1.8 遍历打印顺序表

cs 复制代码
void display(sqlist L) {
    for (int i = 0; i < L.length; i++) {
        printf("%d ", L.elem[i]);  
    }
    printf("\n"); 

1.9 完整代码

cs 复制代码
#include<stdio.h>
#include<stdlib.h>

typedef struct {
	int *elem;
	int length;
	int listsize;
} sqlist;

// 初始化顺序表
int init(sqlist &L,int n) {
	L.elem=(int*)malloc(100*sizeof(int));
	if(!L.elem) return 0;  // 内存分配失败
	L.length=0;
	for(int i=0; i<n; i++) {
		scanf("%d",&(L.elem[i]));
		L.length++;
	}
	L.listsize=100;
	return 1;
}

// 插入元素
int insert(sqlist &L,int i,int e) {
	int *newspace;
	int *p;
	int *q;
	if(i<1||i>L.length+1) return 0;  // 位置不合法
	if(L.length>=L.listsize) { // 需要扩容
		newspace=(int*)realloc(L.elem,(L.listsize+10)*sizeof(int));
		if(!newspace) return 0;  // 扩容失败
		L.elem=newspace;
		L.listsize+=10;
	}
	q=&(L.elem[i-1]);  // 插入位置
	// 从后往前移动元素
	for(p=&(L.elem[L.length-1]); p>=q; --p) {
		*(p+1)=*p;
	}
	*q=e;  // 插入新元素
	++L.length;
	return 1;
}

// 删除元素
int dele(sqlist &L,int i,int &e) {
	int *q;
	int *p;
	if(i<1||i>L.length) return 0;  // 位置不合法
	p=&(L.elem[i-1]);
	e=*p;  // 保存要删除的元素
	q=L.elem+L.length-1;  // 最后一个元素位置
	// 从删除位置往后移动元素
	for(++p; p<=q; ++p) *(p-1)=*p;
	--L.length;
	return 1;
}

// 查找元素
int locate(sqlist L,int e) {
	int i=1;
	int *p=L.elem;
	while((i<=L.length)&&(*p!=e)) {
		i++;
		p++;
	};
	if(i<=L.length) return i;  // 找到返回位置
	else return 0;  // 未找到返回0
}

// 显示链表内容
void display(sqlist L) {
	for(int i=0; i<L.length; i++)
		printf("%d ",L.elem[i]);
	printf("\n");
}

int main() {
	sqlist p;
	int i,e,n;
	int s;  // 操作选择
	printf("输入建立顺序表大小n:\n");
	scanf("%d",&n);
	printf("输入顺序表的值(空格分隔):\n");
	if(!init(p,n)) { // 检查初始化是否成功
		printf("初始化失败!\n");
		return 1;
	}

	printf("建立的顺序表表为:\n");
	display(p);

	do {
		printf("\n请选择操作:\n");
		printf("1. 插入元素\n");
		printf("2. 删除元素\n");
		printf("3. 查找元素\n");
		printf("0. 退出程序\n");
		scanf("%d",&s);  // 修正:添加取地址符&

		switch(s) {
			case 1:
				printf("请输入插入位置和元素值(用空格分隔):\n");
				scanf("%d%d",&i,&e);
				if(insert(p,i,e)) {
					printf("插入成功!当前链表为:\n");
					display(p);
				} else {
					printf("插入失败!位置不合法或内存不足\n");
				}
				break;

			case 2:
				printf("请输入要删除的位置:\n");
				scanf("%d",&i);
				if(dele(p,i,e)) {
					printf("删除成功!删除的元素是:%d\n",e);
					printf("当前链表为:\n");
					display(p);
				} else {
					printf("删除失败!位置不合法\n");
				}
				break;

			case 3:
				printf("请输入要查找的元素值:\n");
				scanf("%d",&e);
				i = locate(p,e);
				if(i!=0) {
					printf("元素%d的位置是:%d\n",e,i);
				} else {
					printf("未找到元素%d\n",e);
				}
				printf("当前链表为:\n");
				display(p);
				break;

			case 0:
				printf("操作结束,退出程序\n");
				break;

			default:
				printf("输入错误,请重新选择\n");
		}
	} while(s!=0);

	// 释放动态分配的内存
	free(p.elem);
	return 0;
}

1.10 顺序表的优缺点

顺序表优点

存储密度大,存储效率高

顺序表采用连续的内存空间存储数据,无需额外的指针或链接信息,存储密度接近100%,空间利用率高。

随机访问快速

通过下标可在 O(1) 时间内直接访问任意元素,适合频繁查询或需要快速定位的场景。

顺序表缺点

插入,删除效率低

在非尾部位置插入或删除元素时,需要移动大量元素以保证连续性,时间复杂度为 O(n)。

固定容量限制
静态存储形式,元素个数不能自由扩充。静态分配内存的顺序表需预先指定最大容量,可能导致空间浪费或溢出;动态分配虽可扩容,但需复制全部数据,开销较大。

2.链表特点与基本操作

2.1 链表的定义

链表是一种线性数据结构,由一系列节点(Node)组成,每个节点包含两部分:

  • 数据域:存储实际的数据。
  • 指针域 :存储指向下一个节点的地址(或引用)。
    链表通过指针将节点按逻辑顺序连接,形成链式结构。

2.2 链表的特点

动态内存分配

  • 链表的节点在内存中不必连续存储,通过指针动态关联,因此长度可灵活调整。

插入与删除高效

  • 时间复杂度为 O(1)(若已知操作位置的前驱节点)。
  • 无需像数组那样移动大量元素。

随机访问效率低

  • 需从头节点开始遍历,时间复杂度为 O(n)

链式存储结构

  • 节点在存储器上的位置是任意的,逻辑上相邻的元素在物理上不一定会相邻。
  • 节点内部存储空间不连续,节点之间存储空间连续。

2.3单链表的定义与表示

cs 复制代码
// 定义单链表节点结构体
typedef struct lnode{
    int data;               // 节点存储的数据
    struct lnode *next;     // 指向后继节点的指针
}lnode, *link;              // lnode为节点类型,link为节点指针类型(指向lnode的指针)

2.4 创建链表

2.4.1 逆序创建(头插法)

cs 复制代码
/**
 * 逆序创建单链表(头插法)
 * 功能:输入n个元素,按逆序插入到链表中(新元素始终插在头节点之后)
 * @param L 链表头指针的引用(通过引用修改实参的头指针)
 * @param n 要创建的链表节点个数
 * @return 1-创建成功
 */

int f_creat(link &L,int n){
    int i;
    struct lnode *p;  // 临时指针,用于指向新创建的节点
    
    // 创建头节点
    L=(link)malloc(sizeof(lnode));
    L->next=NULL;     // 头节点初始后继为NULL(空链表状态)
    
    // 循环创建n个节点
    for(int i=0;i<n;i++){
        p=(link)malloc(sizeof(lnode));  // 为新节点分配内存
        scanf("%d",&p->data);          
        
        // 头插法核心:新节点的后继指向当前头节点的后继
        p->next=L->next;
        // 头节点的后继指向新节点(将新节点插入到头节点之后)
        L->next=p;
    }
    return 1;  // 创建成功
} 

2.4.2 正序创建(尾插法)

cs 复制代码
/**
 * 正序创建单链表(尾插法)
 * 功能:输入n个元素,按输入顺序依次插入到链表尾部,最终链表顺序与输入顺序一致
 * @param L 链表头指针的引用(用于修改外部头指针)
 * @param n 要创建的节点个数
 * @return 1-创建成功
 */

int t_creat(link &L, int n) {
    // 1. 创建头节点
    L = (link)malloc(sizeof(lnode));  
    L->next = NULL;                    // 头节点初始后继为NULL(空链表状态)
    
    // 2. 定义尾指针p,初始指向头节点(此时头节点是链表的最后一个节点)
    link p = L;                       
    
    // 3. 循环创建n个节点,并按顺序插入到链表尾部
    for (int i = 0; i < n; i++) {
        // 创建新节点s
        link s = (link)malloc(sizeof(lnode));  
        scanf("%d", &s->data);                 
        // 将新节点s链接到当前尾节点p的后面
        p->next = s;                        
        // 尾指针p移动到新节点s(更新尾节点为s)
        p = s;  
    }
    // 4. 链表创建完成后,将最后一个节点的后继置为NULL(标记链表结束)
    p->next = NULL;
    
    return 1;  
}

2.5 获取元素

cs 复制代码
/**
 * 获取链表中第i个元素的值
 * @param L 链表头指针(头节点)
 * @param i 要获取的元素位置(逻辑位置,从1开始计数)
 * @param e 用于存储获取到的元素值(输出参数)
 * @return 1-获取成功;0-位置不合法(i超出范围)
 */
int get(link L,int i ,int &e){
    link p;       // 遍历指针,用于指向当前节点
    p=L->next;    // 从第一个数据节点(头节点的后继)开始遍历
    int j=1;      // 记录当前遍历到的位置(初始为第一个节点)
    
    // 循环找到第i个节点:p不为空且未到达第i个节点时继续后移
    while(p && j<i){
        p=p->next;  // 指针后移,指向下一个节点
        j++;        
    }
    
    if(!p || j>i) return 0;
    
    e=p->data;  // 找到第i个节点,将数据存入e
    return 1;   // 获取成功
}

2.6 插入元素

在第i个位置插入元素e,要先找到第i-1个位置

核心步骤:

cs 复制代码
 s->next=p->next;  
 p->next=s;   

上述两行代码不能交换位置

cs 复制代码
/**
 * 在链表第i个位置插入元素e
 * @param L 链表头指针(头节点)
 * @param i 插入位置(逻辑位置,从1开始,可插在表尾)
 * @param e 要插入的元素值
 * @return 1-插入成功;0-位置不合法
 */

int insert(link L,int i,int e){
    struct lnode *s;  // 指向新创建的插入节点
    struct lnode *p;  // 遍历指针,用于找到第i-1个节点(插入位置的前驱)
    int j=0;          // 记录当前位置(初始为头节点,位置0)
    p=L;              // 从头部点开始遍历
    
    // 找到第i-1个节点:p不为空且未到达目标位置时继续后移
    while(p && j<i-1){
        p=p->next;  // 指针后移
        j++;        // 位置计数加1
    }
    
    if(!p || j>i-1) return 0;
    
    // 创建新节点并赋值
    s=(link)malloc(sizeof(lnode));
    s->data=e;
    
    // 插入核心操作:先连后,再连前(避免断链)
    s->next=p->next;  // 新节点的后继指向p的原后继
    p->next=s;        // p的后继指向新节点
    
    return 1;  /
}

2.7 删除节点

核心步骤:

cs 复制代码
p->next=p->next->next

用q记录删除的节点,以便于释放内存

cs 复制代码
/**
 * 删除链表中第i个元素
 * @param L 链表头指针的引用
 * @param i 要删除的元素位置(逻辑位置,从1开始)
 * @param e 用于存储被删除的元素值(输出参数)
 * @return 1-删除成功;0-位置不合法
 */

int del(link &L,int i,int &e){
    struct lnode *p;  // 遍历指针,用于找到第i-1个节点(删除位置的前驱)
    p=L;              // 从头部点开始遍历
    struct lnode *q;  // 指向要删除的节点
    int j=0;          // 记录当前位置(初始为头节点,位置0)
    
    // 找到第i-1个节点:p的后继不为空(确保有第i个节点)且未到达目标位置
    while(p->next && j<i-1){   
        p=p->next;  
        j++;        
    }
    
    // 若p的后继为空(i超过链表长度)或j>i-1(i小于1),则位置不合法
    if(!p->next || j>i-1) return 0;
    
    q=p->next;        // q指向要删除的第i个节点
    p->next=q->next;  // 将p的后继指向q的后继(跳过q,实现删除)
    e=q->data;        // 保存被删除节点的数据
    free(q);          // 释放被删除节点的内存
    return 1;  
}

2.8 遍历打印链表

cs 复制代码
void see(link L){
	struct lnode *p;
	p=L;
	while(p->next)
	{
		p=p->next;
		printf("%d ",p->data);
	}
}

2.9 完整代码

cs 复制代码
#include<stdio.h>
#include<stdlib.h>

typedef struct lnode{
	int data;
	struct lnode *next;
}lnode,*link;

//逆序创建链表 
int f_creat(link &L,int n){
	int i;
	struct lnode *p;
	L=(link)malloc(sizeof(lnode));
	L->next=NULL;
	for(int i=0;i<n;i++){
		p=(link)malloc(sizeof(lnode));
		scanf("%d",&p->data);
		p->next=L->next;
		L->next=p;
	}
	return 1;
} 
//正序创建链表
int t_creat(link &L, int n) {
    L = (link)malloc(sizeof(lnode));  
    L->next = NULL;                
    link p = L;                     
    for (int i = 0; i < n; i++) {
        link s = (link)malloc(sizeof(lnode));  
        scanf("%d", &s->data);                 
        p->next = s;                        
        p=s;  
    }
    p->next=NULL;
    return 1;
}

int get(link L,int i ,int &e){
	link p;
	p=L->next;
	int j=1;
	while(p && j<i){
		p=p->next;
		j++;
	}
	if(!p || j>i) return 0;
	e=p->data;
	return 1;
}

int insert(link L,int i,int e){
	struct lnode *s;
	struct lnode *p;
	int j=0;
	p=L;
	while(p && j<i-1){
		p=p->next;
		j++;
	}
	if(!p || j>i-1) return 0;
	s=(link)malloc(sizeof(lnode));
	s->data=e;
	s->next=p->next;
	p->next=s;
	return 1; 
}

//删除第i个元素
int del(link &L,int i,int &e){
	struct lnode *p;
	p=L;
	struct lnode *q;
	int j=0;
	//p为第i-1个元素 
	while(p->next && j<i-1){   
		p=p->next;
		j++;
	}
	if(!p->next || j>i-1) return 0;
	q=p->next;
	p->next=q->next;
	e=q->data;
	free(q);
	return 1;	
} 

void see(link L){
	struct lnode *p;
	p=L;
	while(p->next)
	{
		p=p->next;
		printf("%d ",p->data);
	}
}
int main()
{
	link p,L,s;
	int  data;
	int i,n,e;
	int select;
	printf("输入链表长度:\n");
	scanf("%d",&n);
	printf("输入链表的值,空格分隔:\n");
	t_creat(L,n);
	see(L);
	do{
		printf("\n1:取出\n");
		printf("2:插入\n");
		printf("3:删除\n");
		printf("0:结束!\n");
		printf("请输入选择\n");
		scanf("%d",&select);
		switch(select)
		{
			case 1:
				printf("输入元素的位置i:\n");
				scanf("%d",&i);
				get(L,i,e);
				printf("取出的元素是:%d\n",e);
				break;
			case 2:
				printf("输入插入元素的位置i和值e,空格分隔\n");
				scanf("%d%d",&i,&e);
				insert(L,i,e);
				printf("插入后所有元素为:\n");
				see(L);
				break;
			case 3:
				printf("输入要删除的元素位次i:\n");
				scanf("%d",&i);
				del(L,i,e);
				printf("删除后所有元素为:\n");
				see(L);
				break;
			case 0:
				printf("操作结束");
				break;
			default: printf("选择出错");
			
		}
	}while(select!=0);
	
	p=L;
    while(p != NULL){
        link temp = p;
        p = p->next;
        free(temp);
    }
    return 0;
}

2.10 链表的优缺点

链表的优点

动态大小

链表不需要预先分配固定大小的内存空间,可以根据实际需求动态调整大小,适合数据量变化频繁的场景。

高效插入和删除

在链表中插入或删除节点只需修改相邻节点的指针,时间复杂度为 O(1)(已知位置时)。相比之下,数组需要移动大量元素,时间复杂度为 O(n)。

内存利用率高

链表节点在内存中不必连续存储,可以充分利用零散的内存空间,避免数组因连续内存分配导致的碎片问题。

灵活的结构扩展

链表可以轻松扩展为双向链表、循环链表等变体,支持更复杂的操作(如反向遍历),而数组结构固定。

链表的缺点

存储密度小,随机访问效率低

链表不支持直接索引访问,查找第 i个元素需要从头节点开始遍历,时间复杂度为 O(n)。数组的随机访问时间为 O(1)。

额外内存开销

每个节点需存储指针(或引用),占用额外内存。例如,单链表每个节点比数组多一个指针的空间开销。

相关推荐
CodeRecycle23 分钟前
Python 自动配置 pip 支持库(通过 Windows Bat 脚本)
算法
liliangcsdn40 分钟前
特质偏度因子的计算示例和分析
算法
荆棘鸟智能1 小时前
无人机遥感图像实时拼接算法工程实践:从特征提取到全景融合的完整技术链路
算法·无人机
xxwxx__2 小时前
C++ STL set 与 map 全套详解:关联式容器、红黑树底层、代码坑点、刷题实战
数据结构·c++
6Hzlia2 小时前
【Classic 150 刷题计划】 LeetCode 58. 最后一个单词的长度 | C++ 极简反向遍历与单变量状态机
算法
AgentMaster2 小时前
企业如何应用智能客服?5 个典型场景的技术架构与实施路径
大数据·算法
程曦曦2 小时前
MySQL 生产库误删 98 张表后的时间点恢复实战:从 binlog 解析到资金对账
linux·数据结构·其他·算法·ubuntu·运维开发
郝学胜-神的一滴2 小时前
Effective Python 条款 13:善用星号解包,告别下标切片拆分的坑
服务器·开发语言·数据结构·python·程序人生·pycharm
Logic1013 小时前
C语言/数据结构位运算题解:异或XOR找出流水线上的“独特零件编号“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
weixin_307779133 小时前
C++代码实现MATLAB中的ode45函数功能
开发语言·c++·算法·matlab