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