第8章 数据结构

数据结构主要研究数据的组织方式以及相应的操作方法。它除了描述数据本身之外, 还描述数据之间的相互关系。它不仅是一般程序设计的基础,而且是设计编译程序、操作 系统、数据库、人工智能及其他大型应用程序的基础。如今,数据结构在计算机科学中占 有重要的地位。对于相当多的程序设计来说,认清数据的内在关系,可获得对问题的正确 认识,看清问题的结构甚至解法。在一定意义上,程序所描述的就是在数据结构上实现的 算法。算法的设计依赖于数据的逻辑结构,算法的实现依赖于数据的存储结构,所以数据 结构选择得好坏,对程序质量的影响甚大。掌握基本的数据结构知识,是提高程序设计水 平的必要条件。
单链表的结构是数据结构中最简单的,它的每一个节点只有一个指向后一个节点的指针,其模型如图8.1所示。
 

循环链表与单链表一样,是一种链式的存储结构;不同的是,循环链表的最后一个节 点的指针指向该循环链表的第一个节点或者表头节点,从而构成一个环形的链。其结构模型如图8.2所示。
 
 
 当对单链表进行操作时,有时你要对某个结点的直接前驱进行操作,又必须从表头开 始查找。由于单链表每个结点只有一个存储直接后继结点地址的链域,因此运用单链表是 无法办到的。那么能不能定义一个既有存储直接后继结点地址的链域,又有存储直接前驱 结点地址的链域的这样一个双链域结点结构呢?有,这就是双向链表。
 在双向链表中,结点除含有数据域外,还有两个指针,一个存储直接后继结点地址, 另一个存储直接前驱结点地址。双向链表如图8.3所示。
 
 双向循环链表其实就是把双向链表的首尾相连,其模型图如图8.4所示。


 面试题1  编程实现一个单链表的建立
 考点:单链表的操作
 出现频率:★★★★
 【解析】
 链表节点的定义:

typedef struct node
{
	int data;				//节点内容
	node* next;				//下一个节点
}node;

单链表的创建:

//创建单链表
node* create()
{
	int i = 0;										//链表中数据的个数
	node* head{nullptr}, * p{ nullptr }, * q{ nullptr };
	int x = 0;
	head = (node*)malloc(sizeof(node));				//创建头节点
	while (1)
	{
		printf("Please input the data:");
		scanf("%d", &x);
		if (x == 0)									//data为0时创建结束
			break;
		p = (node*)malloc(sizeof(node));
		p->data = x;
		if (++i == 1)
		{											 //链表只有一个元素
			head->next = p;							 //连接到head的后面
		}
		else
		{
			q->next = p;							//连接到链表尾端	
		}
		q = p;										//q指向末节点
	}
	q->next = nullptr;								//链表的最后一个指针为NULL
	return head;
}

上面的代码中,使用while循环每次从终端读入一个整型数据,并调用malloc动态分 配链表节点内存存储这个整型数据,然后插入到单链表的末尾。最后,当数据为0时表示 插入数据结束,此时把末尾节点的next指针置为nullptr。

 面试题2   编程实现一个单链表的测长
 考点:单链表的操作
 出现频率:★★★★
【 解析】
 单链表的测长:

/返回单链表长度
int length(node* head)
{
	int len = 0;
	node* p;
	p = head->next;
	while (p != nullptr)			//遍历链表
	{
		len++;
		p = p->next;
	}
	return len;
}

 由于链表末尾节点的next指针被置为nullptr,因此可以使用while循环遍历链表所有节 点,当遇到nullptr时结束循环。

 面试题3  编程实现一个单链表的打印
 考点:单链表的操作
 出现频率:★★★★
 【解析】
 单链表的打印:

//打印单链表
void print(node* head)
{
	node* p;
	int index = 0;
	if (head->next == nullptr)		//链表为空
	{
		printf("Link is empty!\n");
		return;
	}
	p = head->next;
	while (p != nullptr)			//遍历链表
	{
		printf("The %dth node is:%d\n", ++index, p->data);	//打印元素
		p = p->next;
	}
}

单链表的打印与单链表的测长方法类似,使用while循环遍历链表所有节点并打印各个 节点内容,当遇到NULL时结束循环。

 面试题4 编程实现一个单链表节点的查找
 考点:单链表的操作
 出现频率:★★★★
 【解析】
 单链表节点的查找:

//查找单链表pos位置的节点,返回节点指针
//pos从0开始,0返回head节点
node* search_node(node* head, int pos)
{
	node* p = head->next;
	if (pos < 0)					//pos位置不正确
	{
		printf("incorrect position to search node!\n");
		return nullptr;
	}
	if (pos == 0)					//在head位置,返回head
	{
		return head;
	}
	if (p == nullptr)
	{
		printf("Link is empty!\n");		//链表为空
		return nullptr;
	}

	while (--pos)
	{
		if ((p = p->next) == nullptr)
		{								//超出链表返回
			printf("incorrect position to search node!\n");
			break;
		}
	}

	return p;
}

面试题5  编程实现一个单链表节点的插入
 考点:单链表的操作
 出现频率:★★★★
 【解析】
 向单链表中某个位置(第pos个节点)之后插入节点,这里分为插入到链表首部、插入 到链表中间,以及链表尾端3种情况。

//在单链表pos位置处插入节点,返回链表头指针
//pos从0开始计算,0表示插入到head节点后面
node* insert_node(node* head, int pos, int data)
{
	node* item = nullptr;
	node* p{nullptr};

	item = (node*)malloc(sizeof(node));
	item->data = data;
	if (pos == 0)							//插入链表头后面
	{
		item->next = head->next;
		head->next = item;					//head后面是item
		return head;
	}
	p = search_node(head, pos);				//获得位置pos的节点指针
	if (p != nullptr)
	{
		item->next = p->next;				//item指向原pos节点的后一个节点
		p->next = item;						//把item插入到pos的后面
	}
	return head;
}

面试题6编程实现一个单链表节点的删除
 考点:单链表的操作
 出现频率:★★★★
 【解析】
 单链表节点的删除:

//删除单链表的pos位置的节点,返回链表头指针
//pos从1开始计算,1表示阙除head后的第一个节点
node* delete_node(node* head, int pos)
{
	node* item = nullptr;
	node* p = head->next;
	if (p == nullptr) //链表为空
	{
		printf("link is empty!\n");
		return nullptr;
	}
	p = search_node(head, pos - 1);				//获得位置pos的节点指针
	if (p != nullptr && p->next != nullptr)
	{
		item = p->next;
		p->next = item->next;
		delete item;
	}
	return head;
}

下面是上面各个函数的测试程序。

 int main()
{
	node* head = create();						//创建单链表
	int len = length(head);						//测量单链表长度
	printf("Length:%d\n", length(head));
	print(head);								 //打印单链表
	node* find3 = search_node(head, 3);
	cout << "find3====" << find3->data << endl;
	printf("insert integer 5 after 2th node:\n");
	head = insert_node(head, 2, 5);				//在第2个节点之后插入5
	print(head);								//打印单链表
	head = delete_node(head, 2);				//删除第2个节点
	printf("delete the 3th node:\n");
	print(head);								//打印单链表
	cout << "hello world" << endl;
	return 0;
}

程序执行结果:


面试题7 实现一个单链表的逆置

这是一个经常被问到的面试题,也是一份非常基础的问题。比如一个链表是这样的:
1->2->3->4->5通过逆置后成为5->4->3->2->1。
最容易想到的方法是遍历一遍链表,利用一个辅助指针,存储遍历过程中当前指针指向的下一个元素,然后将当前节点元素的指针反转后,利用已经存储的指针往后面继续遍历。代码如下:

node* reverse(node* head)
{
	node* p{ nullptr }, * q{ nullptr }, * r{ nullptr };
	if (head->next == nullptr)			//链表为空
	{
		return head;
	}
	p = head->next;
	q = p->next;						//保存原第2个节点
	p->next = nullptr;					//原第1个节点为末节点

	while (q != nullptr)				//遍历,各个节点的next指针反转
	{
		r = q->next;
		q->next = p;
		p = q;
		q = r;
	}
	head->next = p;						//新的第1个节点为原末节点
	return head;
}

int main()
{
	node* head = create();						//创建单链表
	int len = length(head);						//测量单链表长度
	printf("Length:%d\n", length(head));
	print(head);								 //打印单链表
	head = reverse(head);					     //逆置
	print(head);
	cout << "hello world" << endl;
	return 0;
}

运行结果:

面试题8寻找单链表的中间元素
 考点:单链表的操作
 出现频率:★★★★
 【解析】
 这里使用一个只用一遍扫描的方法。描述如下:
 假设mid指向当前已经扫描的子链表的中间元素,cur指向当前已扫描链表的未节点, 那么继续扫描即移动cur到cur->next,这时只需判断一下应不应该移动mid到mid->next就 行了。所以一遍扫描就能找到中间位置。代码如下。

node* search(node* head)
{
	int i = 0;
	int j = 0;
	node* current = nullptr;
	node* middle = nullptr;

	current = middle = head->next;
	while (current != nullptr)
	{
		if (i / 2 > j)
		{
			j++;
			middle = middle->next;
		}
		i++;
		current = current->next;
	}

	return middle;
}

int main()
{
	node* head = create();						//创建单链表
	int len = length(head);						//测量单链表长度
	printf("Length:%d\n", length(head));
	print(head);								 //打印单链表
	
	//8
	node* middle = search(head);
	cout << "middle====" << middle->data << endl;

	cout << "hello world" << endl;
	return 0;
}

运行结果:

面试题9  单链表的正向排序
 考点:单链表的操作
 出现频率:★★★★
 【解析】
 结构体定义和代码如下。

typedef struct node
{
	int data;				//节点内容
	node* next;				//下一个节点
}node;

node* InsertSort(void)
{
	int data = 0;
	struct node* head = nullptr, * New{ nullptr }, * Cur{ nullptr }, * Pre{nullptr};
	while (1)
	{
		printf("please input the data\n");
		scanf("%d", &data);
		if (data == 0)			//输入0结束
		{
			break;
		}
		New = (struct node*)malloc(sizeof(struct node));
		New->data = data;		//新分配一个 node 节点
		New->next = nullptr;
		if (head == nullptr)
		{						//第一次循环时对头节点赋值
			head = New;
			continue;
		}
		if (New->data <= head->data)
		{
			//head之前插入节点
			New->next = head;
			head = New;
			continue;
		}
		Cur = head;
		while (New->data > Cur->data && Cur->next != nullptr) //找到需要插入的位置
		{
			Pre = Cur;
			Cur = Cur->next;
		}
		if (Cur->data >= New->data)	//位置在中间
		{							//把 New节点插入到Pre和Cur 之间
			Pre->next = New;
			New->next = Cur;
		}
		else
		{							//位置在末尾
			Cur->next = New;		//把New节点插入到Cur 之后	
		}
	}
	return head;
}
  
int main()
{
	node* head = InsertSort();
	print(head);
	return 0;
}  

 运行结果:

面试题10 判断链表是否存在环型链表问题
 考点:单链表的操作
 出现频率:★★★★
 【解析】
 这里有一个比较简单的解法。设置两个指针pl、p2。每次循环pl向前走一步,p2向 前走两步。直到p2碰到NULL指针或者两个指针相等时结束循环。如果两个指针相等,则 说明存在环。
 程序代码如下。

//判断是否存在回环
//如果存在。start存放回环开始的节点
bool IsLoop(node* head, node** start)
{
	node* p1 = head, * p2 = head;
	if (head == nullptr || head->next == nullptr)
	{												//head为NULL或
		return false;								//链表为空时返回false
	}
	do
	{
		p1 = p1->next;								//p1走一步
		p2 = p2->next->next;						//p2走两步
	} while (p2 && p2->next && p1 != p2);

	if (p1 == p2)
	{
		*start = p1;								// p1为回环开始节点
		return true;
	}
	else
	{
		return false;
	}
}

int main()
{
	bool bLoop = false;
	node* head = create();								//创建单链表
	node* start = head->next->next->next;				//使第4个节点为回环开始位置
	start->next = head->next;							//回环连接到第2个节点

	node* loopStart = nullptr;
	bLoop = IsLoop(head, &loopStart);
	printf("bLoop = %d\n", bLoop);
	printf("bLoop == loopStart ? %d\n", (loopStart == start));
	return 0;
}

main()函数中对IsLoop()函数做了测试,这里代码第31行到第32行手动把第2个节点
 接到了原来的第4个节点之后,于是节点4就成了回环开始的节点。因此,第36行和第37 行的两条打印语句输出都是1。

运行结果:

面试题11 有序单链表的合并
 考点:单链表的操作
 出现频率:★★★★
 已知两个链表headl和head2各自有序,请把它们合并成一个链表,依然有序。使用 非递归方法以及递归方法。
 【解析】
 首先介绍非递归方法。因为两个链表headl和head2都是有序的,所以我们只需要把 较短链表的各个元素有序地插入到较长的链表之中就可以了。
 源代码如下。

node* insert_node(node* head, node* item)    //head !=NULL
{
	node* p = head;
	node* q = nullptr;						//始终指向p之前的节点

	while (p->data < item->data && p != nullptr)
	{
		q = p;
		p = p->next;
	}
	if (p == head)							//插入到原头节点之前
	{
		item->next = p;
		return item;
	}
	//插入到q与p之间
	q->next = item;
	item->next = p;
	return head;
}

//两个有序链表进行合并
node* nerge(node* head1, node* head2)
{
	node* head{ nullptr };				//合并后的头指针
	node* p{ nullptr };
	node* nextP{ nullptr };				//指向p之后

	if (head1 == nullptr)				//有一个链表为空的情况,直接返回另一个链表
	{
		return head2;
	}
	else if (head2 == nullptr)
	{
		return head1;
	}

	//两个链表都不为空
	if (length(head1) >= length(head2))		//选取较短的链表
	{										//这样进行的插入次数要少些
		head = head1;
		p = head2;
	}
	else
	{
		head = head2;
		p = head1;
	}

	while (p != nullptr)
	{
		nextP = p->next;						//保存p的下一个节点
		head = insert_node(head, p);			//把p插入到目标链表中
		p = nextP;								//指向将要插入的下一个节点
	}

	return head;
}

这里insert_node()函数是有序的插入节点,注意与前面例题中的函数有区别,这里它传 入的参数是node*类型。然后在merge()函数中(代码第52~55行)循环把短链表中的所有 节点插入到长链表中。
 接下来介绍递归方法。比如有下面两个链表。
 链表1:1->3->5
 链表2:2->4->6
 递归方法的步骤如下。
 (1)比较链表1和链表2的第一个节点数据。由于1<2,因此把结果链表头节点指向链
 表1中的第一个节点,即数据1所在的节点。
 (2)对剩余的链表1(3->5)和链表2再调用本过程,比较得到结果链表的第二个节点,
 即2与3比较得到2。此时合并后的链表节点为1>2。
 接下来的过程类似(2),如此递归,直到两个链表的节点都被加到结果链表中。

ode* insert_node(node* head, node* item)    //head !=NULL
{
	node* p = head;
	node* q = nullptr;						//始终指向p之前的节点

	while (p->data < item->data && p != nullptr)
	{
		q = p;
		p = p->next;
	}
	if (p == head)							//插入到原头节点之前
	{
		item->next = p;
		return item;
	}
	//插入到q与p之间
	q->next = item;
	item->next = p;
	return head;
}

//两个有序链表进行合并
node* merge(node* head1, node* head2)
{
	node* head{ nullptr };				//合并后的头指针
	node* p{ nullptr };
	node* nextP{ nullptr };				//指向p之后

	if (head1 == nullptr)				//有一个链表为空的情况,直接返回另一个链表
	{
		return head2;
	}
	else if (head2 == nullptr)
	{
		return head1;
	}

	//两个链表都不为空
	if (length(head1) >= length(head2))		//选取较短的链表
	{										//这样进行的插入次数要少些
		head = head1;
		p = head2;
	}
	else
	{
		head = head2;
		p = head1;
	}

	while (p != nullptr)
	{
		nextP = p->next;						//保存p的下一个节点
		head = insert_node(head, p);			//把p插入到目标链表中
		p = nextP;								//指向将要插入的下一个节点
	}

	return head;
}

这里insert_node()函数是有序的插入节点,注意与前面例题中的函数有区别,这里它传 入的参数是node*类型。然后在merge()函数中(代码第52~55行)循环把短链表中的所有 节点插入到长链表中。
 接下来介绍递归方法。比如有下面两个链表。
 链表1:1->3->5
 链表2:2->4->6
 递归方法的步骤如下。
 (1)比较链表1和链表2的第一个节点数据。由于1<2,因此把结果链表头节点指向链
 表1中的第一个节点,即数据1所在的节点。
 (2)对剩余的链表1(3->5)和链表2再调用本过程,比较得到结果链表的第二个节点,
 即2与3比较得到2。此时合并后的链表节点为1>2。
 接下来的过程类似(2),如此递归,直到两个链表的节点都被加到结果链表中。

node* MergeRecursive(node* head1, node* head2)
{
	node* head = nullptr;
	
	if (head1 == nullptr)
	{
		return head2;
	}
	if (head2 == nullptr)
	{
		return head1;
	}

	if (head1->data < head2->data)
	{
		head = head1;
		head->next = MergeRecursive(head1->next, head2);
	}
	else
	{
		head = head2;
		head->next = MergeRecursive(head1, head2->next);
	}

	return head;
}

下面是测试程序。

int main()
{
	node* head1 = create();		//创建单链表1
	node* head2 = create();		//创建单链表2
	//node *head = merge(head1, head2);
	node* head = MergeRecursive(head1, head2);
	print(head);

	return 0;
}

运行结果:

 这里使用merge()函数和MergeRecursive()函数测试,结果一致。

 面试题12  约瑟夫问题的解答
 考点:循环链表的操作
 出现频率:★★★★
 编号为1,2,…,N的N个人按顺时针方向围坐一圈,每人持有一个密码(正整数), 一开始任选一个正整数作为报数上限值M,从第一个人开始按顺时针方向自1开始按顺序 报数,报到M时停止报数。报M的人出列,将他的密码作为新的M值,从他在顺时针方 向上的下一个人开始重新从1报数,如此下去,直至所有人全部出列为止。试设计一个程 序求出出列顺序。
 【解析】
 显然当有人退出圆圈后,报数的工作要从下一个人开始继续,而剩下的人仍然是围成 一个圆圈的,因此可以使用循环单链表。由于退出圆圈的工作对应着表中结点的删除操作, 对于这种删除操作频繁的情况,选用效率较高的链表结构。为了程序指针每一次都指向一 个具体的代表一个人的结点而不需要判断,链表不带头结点。所以,对于所有人围成的圆 圈所对应的数据结构采用一个不带头节点的循环链表来描述。设头指针为p,并根据具体情 况移动。
 为了记录退出的人的先后顺序,采用一个顺序表进行存储。程序结束后再输出依次退 出的人的编号顺序。由于只记录各个节点的data值就可以,所以定义一个整型一维数组。 如int quit[n];n为一个根据实际问题定义的一个足够大的整数。
 程序代码如下。

#pragma warning(disable:4996)
#include <iostream>
using namespace std;

//结构体和函数声明
typedef struct node
{
	int data{0};				//节点内容
	node* next;				//下一个节点
}node;

//构造节点数量为n的单向循环链表
node* node_create(int n)
{
	node* pRet = nullptr;
	if (n != 0)
	{
		int n_idx = 1;
		node* p_node = nullptr;

		//构造n个node
		p_node = new node[n];
		if (p_node == nullptr)		//申请内存失败,返回nullptr
		{
			return nullptr;
		}
		else
		{
			memset(p_node, 0, n * sizeof(node));	//初始化内存
		}
		pRet = p_node;
		while (n_idx < n)							//构造循环链表
		{											//初始化链表的每个节点,从1到n
			p_node->data = n_idx;
			p_node->next = p_node + 1;
			p_node = p_node->next;
			n_idx++;
		}
		p_node->data = n;
		p_node->next = pRet;
	}

	return pRet;
}

int main()
{
	node* pList = nullptr;
	node* pIter = nullptr;
	int n = 20;
	int m = 6;

	//构造单向循环链表
	pList = node_create(n);
	
	//Josephus循环取数
	pIter = pList;
	m %= n;
	while (pIter != pIter->next)
	{
		int i = 1;

		//取到第m-1个节点
		for (; i < m - 1; i++)
		{
			pIter = pIter->next;
		}

		//输出第m个节点的值
		printf("%d ", pIter->next->data);

		//从链表中删除第m个节点
		pIter->next = pIter->next->next;
		pIter = pIter->next;
	}
	printf("%d\n", pIter->data);

	//释放申请的空间
	delete[]pList;
	return 0;
}

运行结果:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值