数据结构Chap 2/malloc实战——线性表

一、线性表

1.基本概念

2、基本运算

3、小结 

 二、顺序表(顺序存储)

1.定义:用顺序存储的方式实现线性表

后一个元素地址一定是原函数地址的 location 加上 size(ElementType)

2.顺序表的静态分配

如:int data[128]; //容易造成数据空间的浪费或者溢出。

3.顺序表的动态分配()/ malloc实战

利用指针来动态分配数组。如:

#include <iostream> //输出的头文件
#include <stdlib.h> //malloc和free函数的头文件
using namespace std;
#define InitSize 10//默认的顺序表最大长度
typedef struct {
	int* data;//指示动态分配数组的指针
	int MaxSize;//顺序表的最大容量
	int length;//顺序表的当前长度
}SeqList;//封装这个叫SeqList的结构体
void InitList(SeqList& L)//初始化一个结构体
{
	L.data = (int*)malloc(InitSize * sizeof(int));
	//重新开辟一个初始化长度的内存给到L.data指针
	L.length = 0;//当前顺序表长度为0(没有赋实值)
	L.MaxSize = InitSize;//规定最大长度为默认的10
}
void IncreSize(SeqList& L, int len)
{
	int* p = L.data;//把原来空间的首地址先赋给p,便于后面查找和释放
	L.data = (int*)malloc((L.MaxSize + len) * sizeof(int));
	//重新开辟一个加上len个int长度的内存,并强转换后给data
	//【地址先挪过来,注意现在的L.data是在另一个空间里】
	for (int i = 0; i < L.length; i++)
	{
		L.data[i] = p[i];
	}//把原来顺序表的值挪过来
	L.MaxSize = L.MaxSize + len;//现在顺序表的最大空间增加了len,但是新空间还未赋值
	free(p);//释放原有的空间
}
int main()
{
	SeqList L;
	InitList(L);//建立一个长度为0的空链
	cout << "Before:\n";
	for (int i = 0; i < L.MaxSize; i++)
	{
		L.data[i] = i;//赋初值,填满顺序表
		L.length++;//别忘记这个,否则不会执行IncreSize函数中挪值的操作
		cout << L.data[i] << endl;//输出原有顺序表
	}
	IncreSize(L, 5);//换成另一个更长的内存点,存储原顺序表
	cout << "After:\n";
	for (int i = 0; i < L.MaxSize; i++)
	{
		cout << L.data[i] << endl;
	}//打印现有的顺序表
	return 0;
}

4.顺序表的特点

a.随机访问,即可以在O(1)时间内找到第i个元素。//顺序表---顺序存储,内存相邻

b.存储密度高,每个节点只存储数据元素(不像链式存储还要加上指针/地址)。

c.拓展容量不方便(即使采用动态分配,则拓展过程中的时间复杂度也很高)

d.插入、删除操作不方便,需要移动大量元素。

三、顺序表的插入与删除

1.插入

(1)用法示例

#include <iostream>
using namespace std;
#define MaxSize 10//默认的顺序表最大长度
typedef struct {
	int data[MaxSize];
	int length;
}SeqList;

bool ListInsert(SeqList& L, int i, int e)//在第i位插入e元素
{
	if (i < 1 || i > L.length + 1)//判断插入的地址是否有效,只能从1到length+1的位置插
		return false;
	if (L.length >= MaxSize)//如果位序已经到了静态数组最大长度,那肯定也不能插了
		return false;
	for (int j = L.length; j >= i; j--)//从最后位置开始往后移
		L.data[j] = L.data[j-1];
	L.data[i-1] = e;//注意第3位,在数组里是data[2]
	L.length++;
	return 1;//return true 不能省略
}
int main()
{
	SeqList L;
	L.length = 0;//必须有个初值,不然会是一个之前的废弃值
	for (int i = 0; i < 5; i++)
	{
		L.data[i] = i;
		cout << L.data[i] << endl;
		L.length++;
	}
	bool decide = ListInsert(L, 3, 3);
	cout << decide << endl;
	for(int i=0;i<L.length;i++)
		cout << L.data[i] << endl;
	return 0;
}

 (2)时间复杂度

2.删除

(1)用法示例

#include <iostream>
using namespace std;
#define MaxSize 10
typedef struct {
	int data[MaxSize];
	int length;
}SeqList;

bool ListDelete(SeqList& L, int i, int& e)
/*--------在第i位删除的元素带出来赋值给e,注意这里必须用&e,否则无法带出第i位
(实际上第i位赋给了ListDelete函数的新变量e,而并不是主函数中定义的e,不带&的话,
系统会提示带出的值还是-1!)--------*/
{
	if (i < 1 || i > L.length )//已有的第1位到第length位可以删除
		return false;
	e = L.data[i - 1];//将第i位取出来给e(注意下标为位序-1)
	for (int j = i; j < L.length; j++)
		/*-------从第i+1位到第length位都要往前移动,对应数组下标从i到length-1  ----------*/
		L.data[j - 1] = L.data[j];
	L.length--;
	return 1;//return true 不能省略
}
int main()
{
	SeqList L;
	L.length = 0;//必须有个初值,不然会是一个之前的废弃值
	cout << "Before:\n";
	for (int i = 0; i < 5; i++)
	{
		L.data[i] = i;
		cout << L.data[i] << endl;
		L.length++;
	}
	int tmp=-1;//分配一块内存,用于存放取出来的第i位,按理说赋值与否不会影响结果
	if (ListDelete(L, 4, tmp))
		printf("The element has been deleted,and the value is %d\n", tmp);
	else
		cout << "Fail!\n";
	cout << "After:\n";
	for (int i = 0; i < L.length; i++)//输出第1位至第length位,对应数组0到length-1
		cout << L.data[i] << endl;
	return 0;
}

(2)时间复杂度

最好情况O(1),最坏情况O(n),平均情况O((n-1)/2),即O(n)。

3.按位查找

(1)用法示例:

省略顺序表封装和创建链条等操作

int GetElem(SeqList L,int i)
{
    if (i < 1 || i > L.length )
		return -10000;
    return L.data[i-1];/返回第i位,数组下标为i-1
}
int main()
{
    SeqList L;
	InitList(L);//建立一个长度为0的空链
	cout << "Before:\n";
	for (int i = 0; i < L.MaxSize; i++)
	{
		L.data[i] = i;
		L.length++;
		cout << L.data[i] << endl;//输出原有顺序表
	}
    cout<<GetElem(L,3)<<endl;
    return 0;
}

(2)时间复杂度:O(1)

4.按值查找

(1)用法示例:

int LocateElem(SeqList L,int e)
{
    for(int i=0;i<L.length;i++)
        if(L.data[i]==e)
           return i+1;
    return 0;//若没找到,返回0,表示fail
}//注意这里只能比较元素,而不能比较结构体
//(实际上比较结构体是否相同需要将其元素分量一一比较)
int main()
{
    SeqList L;
	InitList(L);//建立一个长度为0的空链
	cout << "Before:\n";
	for (int i = 0; i < L.MaxSize; i++)
	{
		L.data[i] = i;
		L.length++;
		cout << L.data[i] << endl;//输出原有顺序表
	}
    cout<<LocateElem(L,9)<<endl;
    return 0;
}

(2)时间复杂度

最好情况O(1),最坏情况O(n),平均情况O((n-1)/2),即O(n)。

四、链表(链式存储)

1.单链表定义及与顺序表的区别

单链表的各个除了存放数据元素外,还要存储指向下一个结点的指针。(每个结点有一个指针域)

顺序表:优点:可随机存取,存储密度高

              缺点:要求大片连续空间,改变容量不方便

单链表:优点:不要求大片连续空间,改变容量方便

               缺点:不可随机存储,而是顺序存取,要耗费一定空间存放指针

2.代码实现

2.1 链表开头插入数据(头插法)【注意变量范围】

(1)将head作为全局变量

(所有函数之前提前声明 Node* head=NULL,空指针,还未加结点)

#include <iostream>
using namespace std;
typedef struct Node
{
	int data;
	Node* next;
}Node;
Node* head;
void Print(int x)
{
	Node* temp = head;//只需要声明一个指向结点的指针,不用生成新结点
	while(temp != NULL)//注意这里用的应该是while不是if
	{
		cout << temp->data << endl;
		temp = temp->next;
	}
}
void Insert(int x)
{
	Node* temp = new Node;//插入需要新结点,所以需要先生成内存
	temp->data = x;
	temp->next = NULL;
	temp->next = head;//让原来首结点的“地址”储存到temp->next,如果直接写temp=head,则可能让temp也“指向”首结点
	head = temp;
}
int main()
{
	head = NULL;
	cout << "How many numbers do you want to store?" << endl;
	int i;
	cin >> i;
	int x;
	for (int cnt = 0; cnt < i; cnt++)
	{
		cout << "Please enter the number:\n";
		cin >> x;
		Insert(x);
		Print(x);
	}
	return 0;
}
(2)将head作为主函数的局部变量

这样传入插入和打印程序的是head的复制品,即头结点的地址。(注意返回后Print函数需要return,主函数需要让返回值赋值给head)

#include <iostream> //输出的头文件
using namespace std;
#define InitSize 10//默认的顺序表最大长度
typedef struct Node
{
	int data;//数值域
	Node* next;//指针域(我的变量名叫next,变量类型为指向Node的指针,即Node*)
}Node, * LinkList;//使用Node便于对内部数据进行处理
Node* Insert(Node *NewHead ,int x)//输入一个同样指向首结点的新指针
{
	Node* temp = new Node;//temp是指向新结点的指针(也就是新结点的地址)
	temp->data = x;//也就是(*temp).data=x;
	temp->next = NULL;
	//if (head != NULL)//看是否为空链表,不是的话
	//	temp->next = head;//将头指针储存的地址(下一个结点的地址)赋给新结点的指针域
	temp->next = NewHead;//插入头结点,直接将头指针储存的地址(下一个结点的地址)赋给新结点的指针域
	NewHead = temp;//然后让head储存新结点的地址
	return NewHead;//从这里也可以看出,返回的另一个“新head”的指针(指向的temp)
}
void Print(Node *NewHead,int x)//传入头指针的值,即头结点的地址
//类似于,你输入了一个新指针(存的首结点的地址),不过你指向的也是首结点。
{
	while (NewHead != NULL)//头指针指向的地方不为空,首结点不为空,可以继续输出
	{
		cout<< NewHead->data << endl;
		NewHead = NewHead->next;
	}
}
	int main()
	{
		Node* head= NULL;//先声明头指针指向空链表(注意这里是局部变量,函数调用只改变形参值)
		cout << "How many Nodes do you want to store?" << endl;
		int i;
		cin >> i;
		int x;
		for (int cnt = 0; cnt < i; cnt++)
		{
			cout << "Enter the number:\n";
			cin >> x;
			head = Insert(head,x);
			Print(head,x);
		}
	return 0;
}
(3)head为局部变量,但是采取引用符号传入实参或者直接插入头指针的地址
a.利用引用符号
void Insert(Node* &Head, int x)
...
void Print(Node* &Head, int x)
...

int main()
{
    Insert(head,x);
    Print(head,x);
}
b.利用头指针的地址改变插入函数
void Insert(Node** head, int x)
//此时head是头指针的地址,解引用会变成头指针的内容,即头结点的地址
{
    Node* temp = new Node;//分配内存
    temp->data = x;
    temp->next = NULL;//定义新结点
    if (*head != NULL)//这里*head表示对真正头指针进行解引用,*head就是头结点本身有没有
    temp->next = *head;
    *head =temp;
}
void Print(Node *NewHead,int x)//传入头指针的值,即头结点的地址
//类似于,你输入了一个新指针(存的首结点的地址),不过你指向的也是首结点。
{
	while (NewHead != NULL)//头指针指向的地方不为空,首结点不为空,可以继续输出
	{
		cout<< NewHead->data << endl;
		NewHead = NewHead->next;
	}
}
int main()
{
    Insert(&head,x);
    Print(head,x);
}

注意:

1.Insert函数需要新结点,所以需要先定义新结点的x和temp。

Print函数:若函数定义时候传入的是head实参(全局定义或者head的引用地址),则需要temp指针来帮助保证原链表的完整性。[只需要一个指针,无需生成新结点,故不需要定义新结点。]

2.区分temp和temp->next ,前一个是指向临时结点的指针,也是临时结点的地址

后面是临时结点储存的后一个结点(如果后面不为空的话)的地址。

3.用的是while循环而不是if,因为要多次打印。

2.2 任意次序插入(Insert at nth)

#include <iostream>
using namespace std;
typedef struct Node
{
	int data;
	Node* next;
}Node;
Node* head;
void Insert(int data,int n)//n 是你想要成为的次序
{
	Node* temp1 = new Node;//插入需要新结点,所以需要先生成内存
	temp1->data = data;
	temp1->next = NULL;
	if (n == 1)
	{
		temp1->next = head;//让原来首结点的“地址”储存到temp->next,如果直接写temp=head,则可能让temp也“指向”首结点
		head = temp1;
		return;
	}
	Node* temp2 = head;
	for (int i = 0; i < n-2; i++)//循环走n-2次,如果要变化到第n位,则需要找到它的前一位(n-1),其数组下标n-2
		//比如要插入到第三位,则需要找到第二个结点,
//而temp2指针这会指向的是第一位,所以需要移动1次(n=3,移动1次)
	{
		temp2 = temp2->next;//循环n-2次,让temp2指向插入位置的前一个
	} 
	temp1->next = temp2->next;//后面的都归你
	temp2->next = temp1;//你的地址存给我,以后你在我后面
}
void Print()
{
	Node* temp = head;
	while (temp != NULL)
	{
		cout << temp->data;
		temp = temp->next;
	}
}
int main()
{
	head = NULL;
	Insert(2, 1);//List=2
	Insert(4, 2);//List=24
	Insert(3, 1);//List=324
	Print();
	return 0;
}

 图解:

 这里主要分为两种情况:一是插入的位置有前结点,则需要temp2指向前结点。第二种是插入到第一个结点处,即成为头结点,需要特殊考虑。(变成前面的情况了)

2.3 任意次序删除

相比于插入,删除无需生成新结点,但是需要两个指针:temp1指向需要删除的结点,temp2指向其前一个结点。

#include <iostream>
using namespace std;
typedef struct Node
{
	int data;
	Node* next;
}Node;
Node* head;
void Insert(int data)//(尾插法)
{
	Node* temp1 = new Node;
	temp1->data = data;
	temp1->next = NULL;
	if(head == NULL)//没有头结点的情况下,直接插入
	{
		temp1->next = head;
		head = temp1;
		return;
	}
	Node* temp2 = head; //反之,在有尾结点时,需要用temp2找到尾结点
	while (temp2->next != NULL)
		temp2 = temp2->next;
	temp2->next = temp1;//尾部插入
}
void Print()
{
	Node* temp1 = head;//定义临时指针变量依次输出
	while (temp1 != NULL)
	{
		cout << temp1->data << endl;
		temp1 = temp1->next;
	}
}
void Delete(int &n)
{
	struct Node* temp1 = head;
	if (n == 1)
	{
		head = head->next;
		delete temp1;
		return;
	}
	for (int i = 0; i < n - 2; i++)//将temp1指向删除目标的前一个
		temp1 = temp1->next;
	Node* temp2 = temp1->next;//将temp2指向需要删除的目标(注意该目标未被使用时需要delete)
	temp1->next = temp2->next;//删除目标的后一个的地址,赋值给删除目标前一个的指针域
	//即从前一个直接跳到后一个
	delete temp2;
}
int main()
{
	head = NULL;
	Insert(2);//List=2
	Insert(4);//List=24
	Insert(3);//List=243
	Print();
	cout << "Please enter a position to delete:\n";
	int x;
	cin >> x;
	Delete(x);
	Print();
	return 0;
}

2.4 反转一个链表

2.4.1 迭代法
#include <iostream>
using namespace std;
typedef struct Node
{
	int data;
	Node* next;
}Node;
Node* head;
void Insert(int data)//在尾部插入
{
	Node* temp1 = new Node;
	temp1->data = data;
	temp1->next = NULL;
	if (head == NULL)
	{
		temp1->next = head;
		head = temp1;
		return;
	}
	Node* temp2 = head; //反之,在有尾结点时,需要用temp2找到尾结点
	while (temp2->next != NULL)
		temp2 = temp2->next;
	temp2->next = temp1;//尾部插入
}
void Reverse()
{
	Node* current = head;
	Node* pre = NULL;
	Node* next;
	//Node* next = current->next;不能在外面声明,因为下一次还需要让next移动
	while(current!=NULL)
	{
		next = current->next;//让next移动到下一结点,记录下一结点的地址
		current->next = pre;//让current指针域存放前一结点的地址
		pre = current;//让pre指向当前地址
		current = next;//current指向下一结点地址。
	}
	head = pre;//头指针指向原来的最后一个结点,完成迭代的反转!
}
void Print()
{
	Node* temp1 = head;//定义临时指针变量依次输出
	while (temp1 != NULL)
	{
		cout << temp1->data << endl;
		temp1 = temp1->next;
	}
}
int main()
{
	head = NULL;
	Insert(2);//List=2
	Insert(4);//List=24
	Insert(3);//List=243
	Print();
	Reverse();
	Print();
	return 0;
}

2.4.2 递归法
(1)逆序输出
#include <iostream>
using namespace std;
typedef struct Node
{
	int data;
	Node* next;
}Node;
Node* Insert(Node* head, int data)//在尾部插入
{
	Node* temp1 = new Node;
	temp1->data = data;
	temp1->next = NULL;
	if (head == NULL)//没有头结点的情况下,直接插入
	{
		temp1->next = head;
		head = temp1;
		return head;
	}
	Node* temp2 = head; //反之,在有头结点时,需要用temp2循环来找到尾结点
	while (temp2->next != NULL)
		temp2 = temp2->next;
	temp2->next = temp1;//尾部插入
	return head;
}
void Print(Node* p)
{
	while (p != NULL)
	{
		cout << p->data << endl;
		p = p->next;
	}
}
//void RevPrint(Node* p)//逆向打印
{
	if (p == NULL)
		return;
	RevPrint(p->next);//先递
	cout << p->data;//后归
}
void Reverse(Node* p)//定义一个向后遍历的指针p
{
	if (p->next == NULL)
	{
		head = p;
		return;
	}//前面反转完了,这是最终的退出条件
	reverse(p->next);//如果第一个结点上式成立,则最后实现后面几句,以此类推
	Node* q = p->next;//p指向倒数第二个结点,首先运行这几句
	q->next = p;//q中存p的地址,实现反向;
	p->next = NULL;//倒数第二个结点指向空。接下来轮到p指向倒数第三个结点的这几句
}
int main()
{
	Node* head = NULL;
	head = Insert(head, 2);//List=2
	head = Insert(head, 4);//List=24
	head = Insert(head, 3);//List=243
	Print(head);
	RevPrint(head);
	return 0;
}

注意:使用局部变量时,需要改变的地方:

(1)主函数返回值(Node*)、主函数输入值(Node* head)

主函数需要return head

(2)调用插入函数时,需要返回:head=Insert(head,3),否则无法插入

(3)PrintRevPrint函数只需打印无需返回head

(2)链表反转
#include <iostream>
using namespace std;
typedef struct Node
{
	int data;
	Node* next;
}Node;
Node* head;
Node* Insert(Node* head, int data)//在尾部插入(需要返回头指针地址)
{
	Node* temp1 = new Node;
	temp1->data = data;
	temp1->next = NULL;
	if (head == NULL)//没有头结点的情况下,直接插入
	{
		temp1->next = head;
		head = temp1;//这里如果不要temp1的话会造成资源的浪费
		return head;
	}
	Node* temp2 = head; //反之,在有头结点时,需要用temp2循环来找到尾结点
	while (temp2->next != NULL)
		temp2 = temp2->next;
	temp2->next = temp1;//尾部插入
	return head;
}
void Reverse(Node* p)//这里不是把head带进去变,而是变了一个临时指针p!!
{
	if (p->next == NULL)// exit condition,没有后结点,执行括号内
	{
		head = p;//head point at the last Node
		return ;
	}
	Reverse(p->next);//有后结点时,递出去,同时执行后面三句
//这里注意,如果p如图所示为倒数第二个,则递出去的是最后一个
//下三句执行时,p指向的还是倒数第二个
	Node* q = p->next;//p在前,q在后
	q->next=p;//让p作为q的下一个
	p->next = NULL;//而p指向空
}
void Print(Node* p)
{
	while (p != NULL)
	{
		cout << p->data << endl;
		p = p->next;
	}
}
int main()
{
	head = NULL;
	head = Insert(head, 2);//List=2
	head = Insert(head, 4);//List=24
	head = Insert(head, 3);//List=243
	Print(head);
	Reverse(head);
	Print(head);
	return 0;
}

注意:这里head是全局变量,head=p这个条件应该出现在最内层(p->next==NULL),这样只是p和q在递归,不会改变head的位置。

递归部分同样可以写成下面的方式:

Node* Reverse(Node* head)
{
	if (head->next == NULL)// exit condition
		return head;
	Node* last = Reverse(head->next);
	head->next->next=head;//这里无法写成head.next.next
//可能是因为head只能被当作头指针(也没初始化结点的原因)
	head->next = NULL;
	return last;
}

 注意:这种可以返回地址的函数,不用考虑全局变量还是局部变量,因为局部变量和全局变量可以同名,但是它们代表不同的变量。 当同名的变量在同一个作用域中存在时,局部变量将会隐藏全局变量。 在代码块或函数内部,使用该变量名时,优先使用的是局部变量。所以传入的相当于也是一个代号。

如果是反转前n个链表呢?

如何递归反转链表 - 知乎 (zhihu.com)

五、双链表 

1.定义:

结点除了data和next指针,还有pre/prior指针

2.优缺点:

好处是更方便地找到前一个结点,在某些(如删除结点)操作起来,更方便

坏处是每个结点占用空间更大,且指针更多容易出错

3.头插法:

(在头结点后面插)

#include <iostream>
using namespace std;
typedef struct Node
{
	int data;
	Node* next;
	Node* pre;
}Node;
Node* head;
void InsertAtHead(int data)//在头部插入
{
	Node* temp = new Node;
	(*temp).data = data;
	temp->next = NULL;
	temp->pre = NULL;
	if (head == NULL)
	{
		head = temp;
		return;  //base case不要忘记返回!
	}
	head->pre = temp;//我是你前者
	temp->next = head;//你在我后面
	head = temp;//我前面就是头指针
}
void ReversePrint(Node* p)//这里p就相当于声明除了一个临时变量temp
{
	if (p->next == NULL)// 空链表直接退出
		return;
	while (p->next != NULL)//后面有结点,往后移动
		p = p->next;//移动到最后尾结点
	while (p != NULL)//从尾结点开始打印,指针往前移动
	{
		cout << p->data << endl;
		p = p->pre;
	}
}
void Print(Node* p)
{
	while (p != NULL)
	{
		cout << p->data << endl;
		p = p->next;
	}
}
int main()
{
	head = NULL;
	InsertAtHead(2);//List=2
	InsertAtHead(4);//List=42
	InsertAtHead(3);//List=342
	Print(head);
	ReversePrint(head);
	return 0;
}

 4.后插法:

#pragma once
#include "iostream"
#include <cstring>
using namespace std;
typedef struct DNode
{
	int data;
	DNode* next;
	DNode* pre;
}DNode;
DNode* head;
bool InsertNextDNode(DNode *p,DNode *s)//输入两个指向双链表结点的指针p和s
{
	if (p == NULL || s == NULL)
		return false;
	s->next = p->next;//你后面的给我
	if (p->next != NULL)//如果后续结点为空,就不用管下一个结点的pre指针了
		p->next->pre = s;
	p->next = s;//我在你后面
	s->pre = p;//你在我前面
	return true;
}

 ps:单链表无法使用前插法,而双链表很容易实现,只需找到前驱结点,使用后插法即可。

5.双链表的删除

(删除p结点的后继结点)

bool DeleteNextDNode(DNode* p)
{
	if (p == NULL)//本身结点不能为空吧
		return false;
	DNode* q = p->next;
	if (q == NULL)//我作为你的后继结点,我不能为空吧
		return false;
	p->next = q->next;//无论q后面是NULL还是后面还有结点,都可以把q后面的地址存到p->next
	if (q->next != NULL)//如果q后面有结点,才能将p存入后结点的pre中
		q->next->pre = p;
	free(q);
	return true;
}

  (在上述基础上,删除整个链表)

void DestroyList(DNode* head)
{
	while (head->next != NULL)//只要头结点后面还有值,就一个一个删,知道只剩头结点
		DeleteNextDNode(head);
	free(head);//释放头结点空间,头指针还在
	head= NULL;//让头指针指向空,防止野指针
}

         总结:

        (1)双链表不可随机存取,按位查找(在遍历的基础上添加一个计数器)、按值查找(数值对比一下)操作只能通过遍历的方式实现。时间复杂度O(n)。

        (2)双链表需要特别注意操作结点是不是尾结点,如果是的话需要特殊处理。

 六、循环链表

1.循环单链表

1.定义:尾结点指针L.next再指向NULL,而是指向头结点。(单结点的话,next指针指向自己)

2.作用:相比单链表,循环单链表可以从一个结点出发,找到其他任意一个结点。【循环遍历找到任意一个结点】

3.循环单链表:从头找到尾的时间复杂度O(n),从尾找到头的时间复杂度O(1).

ps:可以让一个指针L指向表尾结点【插入、删除时只需修改L即可】

2.循环双链表

1.定义:循环双链表的表头结点prior指向表尾结点;表尾结点的next指向头结点。

2.初始化循环双链表时,让头结点的prior和next结点都指向自己(区别于普通双链表前后指针均指向NULL)

3.双链表的插入和删除:直接缝补就行,不需要考虑操作结点是否为尾结点

4.一些代码问题:

 判空:只剩头结点(但不是数据结点),if(L->next==L)

表头(第一个数据结点):if(p->pre==head);

表尾(最后一个数据结点):if(p->next==head);

删除结点的答案见序号3.

七、静态链表(结构体数组)

1.定义

分配一整片连续的内存空间,各个结点集中安置,内存是连续的。顺序却是由下一个结点的数组下标(游标)决定【这里游标充当了指针的角色,不过游标不是地址,而是序号】。

【很像顺序表和链表的结合】

ps:如果游标为-1,则表示达到尾结点

2.如何用代码定义一个静态链表

#pragma once
#include <iostream>
using namespace std;
#define MaxSize 10
struct Node
{
	int data;
	int next=-2;//方便系统找到空结点
};
typedef struct
{
	int data;
	int next;//下一个在"结构体数组"的序号
}SLinkList[MaxSize];
void testSLinkList()
{
	struct Node X;
	cout << "sizeX=" << sizeof(X) << endl;
	struct Node a[MaxSize];//结构体数组a
	cout << "sizeA=" << sizeof(a) << endl;
	SLinkList b;//静态链表,实际也是结构体数组
	cout << "sizeB=" << sizeof(b) << endl;
}
int main()
{
	testSLinkList();
	return 0;
}

1.sizeof()是一个单目运算符,不需要头文件

2.注意cpp文件需要有main函数,不然会出现LNK错误,因为程序找不到入口。

总结:静态链表为数组实现的链表。

优点:增删操作不需要大量移动元素。

缺点:不能随机存取,只能从头结点依次往后查找,容量固定不变。

八、总结顺序表和链表

 在呢故

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值