数据结构笔记-线性表

166 阅读13分钟

线性表

一、线性表的顺序表示
1.特点
①逻辑上相邻的元素,物理上也相邻,地址连续
②首元素a1地址为LOC(a1),每个元素占L字节,则LOC(ai+1)=LOC(ai)+L;LOC(ai)=LOC(a0)+L * i;
③顺序表操作:修改、插入(在第i个位置前)、删除、查找、排序
2.法一:静态数组

#define maxlen 100
typedef struct {
        int elem[maxlen];//可存放maxlen个元素的数组
        int length;
        }SeqList;
SqList L;
L.length = n;
//线性表中含有n个元素,第一个元素为elem[0],第n个是L.elem[L.length-1],表的最后一个位置是L.elem[maxlen-1]

3.法二:指针数组
注:所有函数里的参数SqList &L,带&是为了可以真正修改L,不用返回L的函数就不用加&
1.首先,用一个结构体来维护顺序表

#define LIST_INIT_SIZE 100//指针数组初始申请空间的大小
#define LISTINCREMENT 10//指针数组每次扩大空间的大小
#define OK 1
#define OVERFLOW -2

#include <stdio.h>
#include <stdlib.h>
typedef struct {
	int* elem;//存储空间基地:该数据得到的内存分配的起始地址,也是数组名
	int length;//表中实际含有的元素个数
	int listsize;//数组的容量
}SqList;
SqList L;

2.建立一个空的顺序表

int InitList(SqList& L) {
	L.elem = (int*)malloc(LIST_INIT_SIZE * sizeof(int));
	if (L.elem == 0) {//表示申请空间失败
		exit(OVERFLOW);//建立空表失败
	}
	L.length = 0;//空表含0个元素
	L.listsize = LIST_INIT_SIZE;//当前用于存放线性表的数组的容量
	return OK;//建立成功
}

3.实现插入元素,这个算法的时间复杂度是O(n)
n个元素,平均移动n/2个元素

int InsertList(SqList& L, int i, int x) {
	int j, * newbase;
	if (i <1 || i > L.length + 1) return -1;//i的合法值为[1, L.length+1]
	if (L.length == L.listsize) {//检查空间是否够用(线性表的长度达到了当前分配的空间大小)
		//空间不够用,扩大空间
		newbase = (int*)realloc(L.elem, (L.listsize + LISTINCREMENT) * sizeof(int));//扩大后的空间大小
		if (newbase == 0) exit(OVERFLOW);//存储分配失败
		L.elem = newbase;//新基址
		L.listsize = L.listsize + LISTINCREMENT;
	}
	//将an~ai顺序向下移动,为新元素让出位置
	//数组下标是0~L.length-1,分别对应a1~an(ai),n=Length,顺序表的最后一位是L.elem[L.listsize-1],它不一定有元素
	for (j = L.length; j >= i; j--) {
		L.elem[j] = L.elem[j - 1];
	}
	L.elem[i - 1] = x;//插入新元素
	++L.length;//修改表长
	return 1;//插入成功
}
//插入的第二种写法
int InsertList2(SqList& L, int i, int x) {
	int* p, * q, *newbase;
	if (i<1 || i > L.length + 1)return -1;
	if (L.length == L.listsize) {
		newbase = (int*)realloc(L.elem, (L.listsize + LISTINCREMENT) * sizeof(int));
		if (newbase == 0) exit(OVERFLOW);
		L.elem = newbase;
		L.listsize = L.listsize + LISTINCREMENT;
	}
	q = &(L.elem[i - 1]);//q记录要插入的位置。即第i个元素的位置
	for (p = &(L.elem[L.length - 1]); p >= q; --p) {//p从后往前找
		*(p + 1) = *p;//插入位置及之后的元素右移
	}
	*q = x;//插入x
	++L.length;
	return OK;
}

4.实现删除元素,时间复杂度为O(n)

int DeleteList(SqList& L, int i) {
	int j;
	if (i < 1 || i > L.length) {
		printf("不存在第i个元素");
		return 0;//要删除的元素不存在
	}
	for (j = i + 1; j < L.length + 1; j++) {
		L.elem[j - 2] = L.elem[j - 1];//将ai+1~an顺序上移,从而删除ai,注意第i个元素是a[i-1]
	}
	L.length--;//修改表长
	return 1;//删除成功
}

实现查找,时间复杂度O(n)

int ListSearch(SqList L, int e) {
	int i;
	for (i = 1; i <= L.length; i++) {
		if (L.elem[i - 1] == e)return i;
	}
	return -1;//没找到e
}

5.线性表就地逆置

void ReverseList(SqList &L) {
	int temp;
	for (int i = 0,j=L.length-1; i <= j; i++,j--) {
		temp = L.elem[i];
		L.elem[i] = L.elem[j];
		L.elem[j] = temp;
	}
}

6.最后写一个主函数调用这些功能

	// 初始化线性表
	InitList(L);
	//创造线性表2,4,6,8,10
	for (int i = 1; i <= 5; i++) {
		InsertList(L, i, i * 2);
	}
	// 打印线性表
	for (int i = 1; i <= L.length; i++) {
		printf("%d ", L.elem[i - 1]);
	}
	printf("\n");
	// 删除第3个元素
	int a = DeleteList(L, 3);
	printf("%d\n", a);
	// 再次打印线性表
	for (int i = 1; i <= L.length; i++) {
		printf("%d ", L.elem[i - 1]);
	}
	printf("\n");
	// 查找值为6的元素
	int index = ListSearch(L, 6);
	if (index != -1) {
		printf("找到值为6的元素,序号为%d", index);
	}
	else {
		printf("未找到值为6的元素");
	}
	return 0;
}

二、线性表的链式表示
1.特点
①地址任意,逻辑相邻不一定物理相邻
②单链表分为不带表头,带表头的,表头结点空着或存特殊信息
③只能顺序存取
④插入和删除操作不需要移动数据
⑤按值查找O(n),和顺序表示的按值查找速度相同;按位置查找O(n),比顺序表示的按位置查找速度慢
⑥线性单链表包括动态链表(指针数据类型)、静态链表(数组)
(一)动态链表
首先认识一下指针类型变量的初始化操作
可以申请空间p = (LinkList)malloc(sizeof(Node));,也可以赋值p = q;
接下来的有些函数,我们写带表头和不带表头两个版本

1.查找结点,时间复杂度O(n)

//带表头
LinkList search(LinkList h, int x) {
	LinkList p;
	p = h->next;//p是第一个元素的地址
	while (p != NULL && p->data != x) {
		p = p->next;
	}
	return p;
}

//不带表头
LinkList search1(LinkList h, int x) {
	LinkList p;
	p = h;//p是第一个元素的地址
	while (p != NULL) {
		if (p->data == x) return p;
		else p = p->next;
	}
	return NULL;
}

//查找第i个元素,将值赋给e,带表头
int GetElem_L(LinkList L, int i, int& e) {
	LinkList p;
	p = L->next;
	int j = 1;
	while (p && j < i) {
		p = p = p->next;
		++j;
	}
	if (!p || j > i) return -1;//第i个元素不存在
	e = p->data;//取第i个元素
	return 1;
}

2.插入结点

void insert(LinkList& p, int x) {
	LinkList s;
	s = (LinkList)malloc(sizeof(Node));
	s->data = x;
	s->next = p->next;
	p->next = s;
}

//带头结点,在第i个位置之前插入e
int ListInsert_L(LinkList& L, int i, int e) {
	LinkList p = L;
	LinkList s;
	s = (LinkList)malloc(sizeof(Node));
	int j = 0;
	while (p && j < i - 1) {
		p = p->next; 
	    ++j; }//寻找第i-1个结点
	if (!p || j > i - 1)return -1;//没找到
	s->data = e;
	s->next = p->next;
	p->next = s;
	return 1;//插入成功
}

3.删除结点

//不带表头
void deleter(LinkList& p) {
	LinkList q;
	if (p->next != NULL)//如果p的直接后继结点存在
	{
		q = p->next;//用q储存被删结点
		p->next = q->next;//删除原来的p->next
		free(q);//释放q的空间
		//为什么要这样转换一下呢?因为如果直接使p->next=p->next->next,想要释放原来被删的p->next的时候,p->next已经换人了,找不到原来的了
	}
}

//带表头,删除值为x的结点
void deleter1(LinkList& h, int x) {
	LinkList p, q;
	p = h->next;
	q = h;
	while (p != NULL) {
		if (p->data == x) {
			q->next = p->next;//要删除的是p
			free(p);
		}
		else
		{
			q = p;
			p = p->next;
			//p和q都右移一位
		}
	}
}

//带头结点,删除第i个元素,由e返回其值
int ListDelete_L(LinkList& L, int i, int& e) {
	LinkList p = L;
	int j = 0;
	while (p->next && j < i - 1) {
		p = p->next;
		++j;
	}//寻找第i个结点,p指向第i-1个结点
	if (!(p->next) || j > i - 1)return -1;//删除位置不合理
	q = p->next;//要删除的是q
	p->next = q->next;//删除q
	e = q->data;
	free(q);
	return 1;//成功
}

拓展:单链表里有很多值为x的结点,都删掉,返回删除的结点数量
整体可调试代码如下

#include <stdio.h>
#include <stdlib.h>
#define NULL 0
#define _CRT_SECURE_NO_WARNINGS

//建立一个头指针为h的,带表头结点的单链表(第一个结点是h,是空的,第二个结点储存第一个元素a1)
typedef struct node {
	int data;
	struct node* next;
}Node, *LinkList;
LinkList h, p;
Node* q;



//带表头,删除值为x的结点
int deleter1(LinkList& h, int x) {
	LinkList p, q, cur;
	if (h == NULL) {
		return 0;  // 空链表,直接返回
	}
	p = h->next;
	q = h;
	int count = 0;
	while (p != NULL) {
		if (p->data == x) {
			q->next = p->next;//要删除的是p
			cur = p->next;//储存p原来的地址
			free(p);
			p = cur;
			count++;
		}
		else
		{
			q = p;
			p = p->next;
			//p和q都右移一位
		}
	}
	return count;
}

//建立单链表
//尾插法,每次在链表尾插入,顺序读入数据,依次尾结点的直接后继
void Create_L2(LinkList& L, int n) {
	LinkList p, s;
	int i;
	L = (LinkList)malloc(sizeof(Node));
	L->next = NULL;//头结点L
	s = L;//尾结点s
	for (i = 1; i <= n; ++i) {//顺序读入a1~an
		p = (LinkList)malloc(sizeof(Node));
		scanf_s("%d", &p->data);//输入元素
		p->next = NULL;
		s->next = p;
		s = p;//p是新的尾结点
	}
}

// 主函数
int main() {
    LinkList L2;

    // 创建链表L2
    printf("Enter the number of elements for L1: ");
    int n;
    scanf_s("%d", &n);
    Create_L2(L2, n);


    // 打印链表L1
    printf("\nL2: ");
    LinkList p = L2->next; // 跳过头结点
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");


    // 删除 L2 中值为 7 的结点
    int count = deleter1(L2, 7);
    printf("\nAfter deleting nodes with value 7 in L2: ");
    p = L2->next; // 跳过头结点
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
	printf("%d", count);

    return 0;
}

//其中,while部分还可以这样写,可以节省一个指针的空间
	while (p != NULL) {
		if (p->data == x) {
			q->next = p->next;//要删除的是p
			free(p);
			p = q->next;
			count++;
		}

4.建立单链表,首插法,尾插法,O(n)

//首插法,每次在*头结点*后插入,逆序读入数据,依次做头结点的直接后继
void Create_L1(LinkList& L, int n) {
	LinkList p;
	int i;
	L = (LinkList)malloc(sizeof(Node));
	L->next = NULL;//头结点L
	for (i = n; i > 0; --i) {//逆序读入a1~an
		p = (LinkList)malloc(sizeof(Node));
		scanf("%d", &p->data);//输入元素
		p->next = L->next;
		L->next = p;//相当于把p插在L和NULL之间
	}
}

//尾插法,每次在链表尾插入,顺序读入数据,依次尾结点的直接后继
void Create_L2(LinkList& L, int n) {
	LinkList p, s;
	int i;
	L = (LinkList)malloc(sizeof(Node));
	L->next = NULL;//头结点L
	s = L;//尾结点s
	for (i = 1; i <= n; ++i) {//顺序读入a1~an
		p = (LinkList)malloc(sizeof(Node));
		scanf("%d", &p->data);//输入元素
		p->next = NULL;
		s->next = p;
		s = p;//p是新的尾结点
	}
}

5.循环单链表
(1)定义:将单链表的尾结点强行指向头结点,带表头就指向表头,不带表头就指向第一个结点
(2)特点:
①从表中任一结点出发均能找到所有结点
②p为尾结点的条件:p->next == h
③循环单链表为空表的判断条件:h->next == h
④空的带表头结点的循环单链表:h指向自己

题目:创建循环单链表,并就地逆置

#include <stdlib.h>
#define _CRT_SECURE_NO_WARNINGS

/* 链表结点定义 */
typedef struct LNode {
	int data; //coef为指数, exp为系数
	struct LNode* next;
}LNode, * LinkList;

//建立循环单链表
void CreatetCircleList(LinkList& L, int n)
{
    if (n <= 0) return; // 如果节点数量小于等于0,则直接返回
    L = (LinkList)malloc(sizeof(LNode)); // 创建头结点
    L->next = NULL;
    LinkList tail = L; // 尾指针指向头结点

    for (int i = 0; i < n; i++)
    {
        int x;
        scanf_s("%d", &x);

        LinkList p = (LinkList)malloc(sizeof(LNode)); // 创建新节点
        p->data = x;
        p->next = NULL;

        tail->next = p; // 将新节点插入到链表尾部
        tail = p; // 更新尾指针指向新的尾节点
    }

    // 将链表头尾相连,形成循环单链表
    tail->next = L;
}

void reverse(LinkList& L) {
    LinkList t = L;
    LinkList p = t->next;
    LinkList q;
    q = p->next;
    while (p != L) {
        p->next = t;
        t = p;
        p = q;
        q = p->next;
    }
    L->next = t;
}

int main() {
    LinkList L;
    CreatetCircleList(L, 5); // 创建循环单链表
    printf("原始链表:\n");
    LinkList p = L->next;
    while (p != L) {
        printf("%d ", p->data); // 输出每个节点的数据值
        p = p->next;
    }
    printf("\n");

    reverse(L); // 反转链表

    printf("逆转后的链表:\n");
    p = L->next;
    while (p != L) {
        printf("%d ", p->data); // 输出每个节点的数据值
        p = p->next;
    }
    printf("\n");

    return 0;
}

6.双向链表
(1)定义:单链表的每个结点包含2个指针(prior,data,next),分别指向结点的直接前驱和直接后继
(2)特点:
①从表中任一结点出发均能找到所有结点(沿两个方向的指针即可)
②插入和删除操作,结点的2个指针均要连接上
③头结点的prior->NULL,尾结点的next->NULL
④空的带表头结点的双向链表:prior和next都指向NULL
空的不带表头结点的双向链表:啥也没有

7.双向循环链表
(1)带表头结点非空:h->prior = tail;tail->next = h;
(2)带表头结点的空的循环双链表:h->next = h; h->prior = h;

8.最后写一个主函数调用以上功能

int main() {
    LinkList L1, L2;

    // 创建链表L1,首插法
    printf("Enter the number of elements for L1: ");
    int n1;
    scanf_s("%d", &n1);
    Create_L1(L1, n1);

    // 创建链表L2,尾插法
    printf("Enter the number of elements for L2: ");
    int n2;
    scanf_s("%d", &n2);
    Create_L2(L2, n2);

    // 打印链表L1
    printf("\nL1: ");
    LinkList p = L1->next; // 跳过头结点
    while (p != NULL) {
        printf("%d\n", p->data);
        p = p->next;
    }

    // 查找值为 5 的结点并打印
    int searchValue = 5;
    LinkList result = search(L1, searchValue);
    if (result != NULL) {
        printf("\nFound node with value %d in L1\n", searchValue);
    }
    else {
        printf("\nNode with value %d not found in L1\n", searchValue);
    }

    // 在 L1 的第二个结点后插入值为 8 的新结点
    p = L1->next; // 重新指向第一个结点
    while (p != NULL && p->data != 2) {
        p = p->next;
    }
    if (p != NULL) {
        insert(p, 8);
        printf("\nAfter inserting 8 after the second node in L1: ");
        p = L1->next;
        while (p != NULL) {
            printf("%d ", p->data);
            p = p->next;
        }
        printf("\n");
    }

    // 删除 L1 的第三个结点后的结点
    p = L1->next; // 重新指向第一个结点
    while (p != NULL && p->data != 3) {
        p = p->next;
    }
    if (p != NULL) {
        deleter(p);
        printf("\nAfter deleting the node after the third node in L1: ");
        p = L1->next;
        while (p != NULL) {
            printf("%d", p->data);
            p = p->next;
        }
        printf("\n");
    }

    // 删除 L2 中值为 7 的结点
    deleter1(L2, 7);
    printf("\nAfter deleting nodes with value 7 in L2: ");
    p = L2->next; // 跳过头结点
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");

    // 释放链表 L1 和 L2 的空间
    p = L1;
    while (p != NULL) {
        LinkList temp = p;
        p = p->next;
        free(temp);
    }

    p = L2;
    while (p != NULL) {
        LinkList temp = p;
        p = p->next;
        free(temp);
    }

    return 0;
}

(二)静态链表
1.创建一个结构体

typedef struct {
	int data;//该元素
	int cur;//下一个元素
}component, SLinkList[MAXSIZE];
//数组的第0分量是头结点

2.查找元素

int LocateElem_SL(SLinkList S, int e) {
	int i = S[0].cur;//i指示表中第一个结点
	while (i && S[i].data != e)//在表中顺链查找
		i = S[i].cur;//相当于指针后移
	return i;
}

各类链表判空条件

带头点的单链表 head->next=NULL
不带头指针的单链表 head=NULL
不带头结点的循环单链表head=NULL

不带头结点的双链表:head = NULL
不带头结点的循环双链表:head = NULL
带表头结点的循环双链表:h->next = h; h->prior = h;

线性表其他操作
现在我们来对链表进行一些其他操作
1.将两个有序链表归并为一个

void MergeList_L(LinkList& La, LinkList& Lb, LinkList& Lc) {
	//La,Lb非递减排序,归并得到Lc也是
	LinkList pa = La->next;
	LinkList pb = Lb->next;
	LinkList pc = La;
	Lc = La;//用La的头结点作为Lc的头结点
	while (pa && pb) {
		if (pa->data <= pb->data) {
			pc->next = pa;
			pc = pa;
			pa = pa->next;
		}
		else {
			pc->next = pb;
			pc = pb;
			pb = pb->next;
		}
	}
	pc->next = pa ? pa : pb;//插入剩余段
	free(Lb);//释放Lb的头结点
}

2.求两个线性表的并集

void unions(LinkList& La, LinkList Lb) {
	//在Lb中且不在La中的元素插入a中
	int La_len = ListLength(La);
	int Lb_len = ListLength(Lb);
	for (i = 1; i < Lb_leng; i++) {
		GetElem(Lb, i, e);//取Lb中第i个元素赋给e
		if(!LocateElem(La,e,equal))ListInsert(La,++La_len,e);//La中不存在和e相同的数据元素,则插入
	}
}

总结一下线性表各种操作的时间复杂度吧

顺序结构
查找:O(1)
插入:O(n)
删除:O(n)//通过下标直接找到待操作元素,主要时间花在移动元素上

链式结构 查找:O(n) 插入:O(n)//主要时间用于找到插入元素的位置 删除:O(n)//主要时间用于找到待删除元素的位置