周小伦说的
建议王道的所有算法题最好都写一下啊,尤其是树的,排序相关的要写一下,然后还有链表,链表有一些反转链表啊
一些经典的代码肯定要背的呀,比如说,三种遍历的递归和非递归,怎么找树的宽度对吧啊,找树的宽度,这是层次便利的对吧,层次便利的东西。然后找就树的宽度一定会找,然后然后啊找就是每个哪个节点在第几层,对吧?这个东西你你也会找,当然这个其实就是dfs就能搞得定。然后呃,除了数的,除了这些之外,
你还比如说排序对吧?你像你快排肯定会写吧,对吧?然后还有冒泡。我觉得堆牌堆牌其实可以不用会,就是快牌和堆牌,你会一个就可以了,其实我觉得背快牌是最好背的。像龟并还,也没有快排好写对吧?快速排序应该是最好写的,然后像链表那边就是被几个经典的,比如说什么怎么反转一个链表对吧?啊,或者说这个反转列表有两种嘛,
一个是可以允许你开辅助空间的,一个是要原地反转,就是要用o1的空间法则把它反转出来。这这些肯定都要,这些都是最基本的,肯定要会吧,对吧?然后像这种经典的双人就是,然后还有除此之外就是把所有期末题里面考核的算法都把它整理一下啊,都记一记,因为考试可能会考到类似的题目啊,会可能会考到非常类似的题目。所以说就是大家这些基本代码都会背的话,你考试的时候基本上也能写点东西上去了,我感觉。
你像很多关数的题目,只要你能够把整棵树遍历完,对吧?什么东西求不出来的,可顶多就是你的算法,它并不是非常优美。对吧,你都能把整个树遍历完了,你还找不到什么祖先了,对吧?那肯那肯定能找得到啊,你遍历树的时候你就能知道每个节点的祖先是什么,对吧?那你把所有所有节点的祖先全存下来,那不就能找到?
找到那两个点的公共祖先了吗?反正就是只要你能够,就很多东西,只要你能够把这个东西遍历完很多,结果都能够找得到,这是最简单一个问题,比如说这个题目你不知道双指针对吧?但是你肯定知道,你可以写两重放循环嘛,你写两重放循环考试的时候12分怎么也给你个八分吧,对吧?那你总不总不可能一个字不写嘛?你像这个找这个对吧?怎么可能?你像这个找找叶子键的个数对吧?
只要你会写后续便利,就只要你会写任何一种便利。你只要把那个便利写出来,然后把里面把里面加上,就是说它的左子数为空的时候把结果加一,你这道题就是满分啊。对吧,但就是最暴力的写法也能得80%以上的分数啊,考试的时候因为它其实不会考,你不会给你卡特别严的,它主要考你数据结构。又不是又不是面试,对吧?就是又不是面试,一定要你写出一个什么显法度的东西来。
第二章 线性表
1.(2013真)已知一个按升序排好的数组和一个数字,请设计一个尽可能高效的算法Findsum,在数据组中查找两个数,使得它们的和正好等于已知的那个数字,例如数组1、2、4、6、7、11和数字11。由于4+7=11,因此输出4和7。如果存在多对这样的数字,输出任意一对即可
#include <stdio.h>
int FindSum(int A[], int n, int key) {
int i = 0, j = n - 1, sum; // 初始化双指针i和j,分别指向数组的开头和结尾
// sum用于记录当前两个数的和
while (i != j && A[i] + A[j] != key) { // 当i和j不相遇且两个数的和不等于key时循环
sum = A[i] + A[j]; // 计算当前两个数的和
if (sum < key)
i++; // 如果和小于key,将i指针右移一位
else
j--; // 如果和大于key,将j指针左移一位
}
if (i == j)
return 0; // 如果i和j相遇,说明没有找到满足条件的数对,返回0
else {
printf("%d和%d\n", A[i], A[j]); // 输出找到的满足条件的数对
}
return 1; // 返回1表示找到了满足条件的数对
}
int main() {
int A[] = {1, 2, 4, 6, 7, 11}; // 已经排好序的数组
int n = sizeof(A) / sizeof(A[0]); // 数组的长度
int key = 11; // 目标数字
FindSum(A, n, key); // 调用函数查找满足条件的数对
return 0;
}
//输出:4和7
这段代码的思路是使用双指针法来查找满足条件的两个数。首先,定义两个指针i和j,分别指向数组的开头和结尾。然后,通过不断调整指针的位置来逼近目标值。在每一次循环中,计算当前指针所指向的两个数的和,并与给定的数字key进行比较。如果和小于key,则将i指针右移一位;如果和大于key,则将j指针左移一位。直到找到满足条件的两个数或者i和j相遇为止。
如果找到了满足条件的两个数,即A[i] + A[j] == key,则输出这两个数。如果不存在这样的数对,即i和j相遇,说明没有找到满足条件的数对,返回0。最后,返回1表示找到了满足条件的数对。
2.(2014期)假设n个结点的单链表head的结点结构为datalnext,请设计一个时间和空间尽可能高效的算法GetLastKth(Link head),找出单链表的中间结点。分析你所设计的算法的时间和空间复杂度
// 定义单链表的节点结构
typedef struct ListNode {
int data; // 节点的数据
struct ListNode* next; // 指向下一个节点的指针
} LinkList;
LinkList findMid(LinkList head) {
LinkList p = head, q = head; // 定义两个指针p和q,初始时都指向链表的头节点head
while (q != NULL && q->next != NULL) { // 当快指针q不为空且q的下一个节点不为空时循环
p = p->next; // 慢指针p向后移动一步
q = q->next->next; // 快指针q向后移动两步
}
return p; // 返回慢指针p,即链表的中间节点
}
//考试不用写
int main() {
// 创建链表节点
LinkList* node1 = (LinkList*)malloc(sizeof(LinkList));
node1->data = 1;
LinkList* node2 = (LinkList*)malloc(sizeof(LinkList));
node2->data = 2;
LinkList* node3 = (LinkList*)malloc(sizeof(LinkList));
node3->data = 3;
LinkList* node4 = (LinkList*)malloc(sizeof(LinkList));
node4->data = 4;
LinkList* node5 = (LinkList*)malloc(sizeof(LinkList));
node5->data = 5;
// 构建链表
LinkList* head = node1;
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = node5;
node5->next = NULL;
// 查找链表的中间节点
LinkList* midNode = findMid(head);
printf("链表的中间节点的值为:%d\n", midNode->data);
return 0;
}
// 结果输出:链表的中间节点的值为3
这个算法使用了快慢指针的思想来找到单链表的中间节点。定义两个指针p和q,初始时都指向链表的头节点head。每次循环中,慢指针p向后移动一步,快指针q向后移动两步。当快指针q到达链表末尾时,慢指针p正好指向链表的中间节点。
这个算法的时间复杂度为O(n),其中n是链表的长度。快指针每次移动两步,慢指针每次移动一步,所以快指针最多需要遍历整个链表的一半,因此时间复杂度为O(n)。
这个算法的空间复杂度为O(1),只使用了常数级别的额外空间。
typedef struct Node {
Elemtype data;
struct Node* next;
} Node;
typedef Node* Link;
// 获取链表的中间节点
Link getMiddleNode(Link head) {
if (head == NULL) { // 如果链表为空,直接返回NULL
return NULL;
}
Link p1 = head; // 快指针,每次移动两步
Link p2 = head; // 慢指针,每次移动一步
while (p1 && p1->next) { // 当快指针p1和p1的下一个节点都不为空时循环
p1 = p1->next->next; // 快指针p1每次移动两步
p2 = p2->next; // 慢指针p2每次移动一步
}
return p2; // 返回慢指针p2,即链表的中间节点
}
3.(2015期)假设由n个整数构成的序列的数据分布为先下降再上升,即一开始数据是严格递减的,后来数据是严格递增的,试设计-一个时间和空间都尽可能高效的算法FindMin,找到序列中的最小值。并分析你所设计算法的时间和空间复杂度
1)最简单思路:从头开始,依次比较相邻元素,当第一次出现前面的元素<后面的元素时,前面那个元素即为最小。
int findMin(int A[], int n){
int i = 0;
for(;i < n;i++){
if(A[i] > A[i+1]){
return A[i];
}
}
}
2)高级思路:运用二分法,当二分点处的值小于左右两点值时,继续二分
代码思路的解释如下:
- 初始化左边界low为0,右边界high为n-1。
- 进入循环,当左边界小于右边界时执行循环体。
- 在循环体中,计算中间位置mid,通过取左边界low和右边界high的平均值。
- 判断A[mid]是否为最小值。如果A[mid]小于其前一个元素A[mid-1]和后一个元素A[mid+1],则A[mid]为最小值,返回A[mid]。
- 如果A[mid]大于A[mid+1],说明最小值在mid的右侧,更新左边界low为mid+1。
- 如果A[mid]小于等于A[mid+1],说明最小值在mid的左侧或就是mid,更新右边界high为mid。
- 循环结束后,左边界和右边界相等,此时指向最小值,返回A[low]即可。
int findMin(int A[], int n) {
int low = 0; // 左边界
int high = n - 1; // 右边界
while (low < high) {
int mid = (low + high) / 2; // 计算中间位置
if (A[mid] < A[mid-1] && A[mid] < A[mid+1]) { // 判断A[mid]是否为最小值
return A[mid]; // 返回最小值
}
else if (A[mid] > A[mid+1]) { // A[mid]大于A[mid+1],最小值在mid右侧
low = mid + 1; // 更新左边界
}
else { // A[mid]小于等于A[mid+1],最小值在mid左侧或就是mid
high = mid; // 更新右边界
}
}
return A[low]; // 返回最小值
}
方法二:gpt版本
int findMin(int A[], int n) {
int left = 0; // 左边界
int right = n - 1; // 右边界
while (left < right) { // 当左边界小于右边界时循环
int mid = left + (right - left) / 2; // 计算中间位置
if (A[mid] > A[mid + 1]) { // 中间元素大于右侧元素,最小值在mid右侧
left = mid + 1; // 更新左边界为mid+1
} else { // 中间元素小于等于右侧元素,最小值在mid左侧或就是mid
right = mid; // 更新右边界为mid
}
}
return A[left]; // 返回左边界位置对应的元素值,即为最小值
}
gpt代码思路步骤:
代码的思路是使用二分查找法来寻找序列中的最小值。具体步骤如下:
- 初始化左边界为序列的首位,右边界为序列的末位。
- 在循环中,计算中间位置mid,通过将左边界和右边界的差值除以2得到中间位置的索引。
- 判断中间元素A[mid]与右侧元素A[mid+1]的大小关系:
- 如果A[mid]大于A[mid+1],说明最小值在mid右侧,因为序列是先下降再上升的。
- 如果A[mid]小于等于A[mid+1],说明最小值在mid左侧或就是mid,因为序列是先下降再上升的。
- 根据判断结果,更新左边界或右边界的值,将查找范围缩小一半。
- 当左边界小于右边界时,继续循环。
- 循环结束后,左边界的位置就是最小值所在的位置。
- 返回左边界位置对应的元素值,即为序列中的最小值。
修正后的代码时间复杂度为O(logn),空间复杂度为O(1)。
如果求最大值的话
int FindMax(int *A, int m) {
if (m == 0) return -1; // 如果数组大小为0,返回错误
int begin = 0;
int end = m - 1;
int MP = (begin + end) / 2;
while (MP > 0 && MP < m - 1) {
if (A[MP] > A[MP+1] && A[MP] > A[MP-1]) { // 如果符合条件就返回此值
return MP;
} else if (A[MP] < A[MP+1]) {
begin = MP + 1; // 在递增段
MP = begin + (end - begin) / 2;
} else {
end = MP - 1; // 在递减段
MP = begin + (end - begin) / 2;
}
}
if (MP == 0) return 0; // 如果数组是完全递减的,则第一个值就是最大值
if (MP == m-1) return m-1; // 如果数组是完全递增的,则最后一个值为最大值
return -1;
}
这个算法的时间复杂度为O(log m),因为每次循环都将搜索空间减半。
空间复杂度为O(1),因为只需要常数级别的额外空间来存储变量。
代码思路解释:
- 首先判断数组大小是否为0,若为0则返回错误。
- 初始化左边界begin为0,右边界end为m-1,计算中间位置MP。
- 进入循环,当中间位置不是第一个元素和最后一个元素时执行循环。
- 在循环体中,判断当前位置的元素与其前一个元素和后一个元素的大小关系。
- 如果当前位置的元素大于其前一个元素和后一个元素,说明找到了最大值,返回最大值的索引。
- 如果当前位置的元素小于后一个元素,说明在递增段,更新左边界为中间位置的后一个位置,更新中间位置。
- 如果当前位置的元素大于等于后一个元素,说明在递减段,更新右边界为中间位置的前一个位置,更新中间位置。
- 循环结束后,如果中间位置为0,则数组是完全递减的,第一个值就是最大值;如果中间位置为m-1,则数组是完全递增的,最后一个值为最大值。
- 如果没有找到最大值,返回错误。
4.(2018真)(13分)在一个长度为n整数序列中,奇数元素和偶数元素各占一半,存放在数组A[n]中。请设计一个时间和空间尽可能高效的算法NewSequence(int A[],int n).重新安排这些整数,使奇数元素存放在奇数单元,偶数元素存放在偶数单元,并说明算法的时间和空间复杂度。
void NewSequence(int A[], int n) {
int i = 0, j = n - 1;
int temp;
while (i < n && j >= 0) { // 循环直到遍历完整个数组
while (i < n && A[i] % 2 == 0) { // 找到第一个奇数元素的位置
i += 2; // 奇数元素存放在奇数单元,所以每次跳两个位置
if (i >= n) { // 如果i超出数组范围,跳出循环
break;
}
}
while (j >= 0 && A[j] % 2 == 1) { // 找到第一个偶数元素的位置
j -= 2; // 偶数元素存放在偶数单元,所以每次跳两个位置
if (j < 0) { // 如果j小于0,跳出循环
break;
}
}
if (i < n && j >= 0) { // 如果i和j都在数组范围内,交换奇数元素和偶数元素
temp = A[i];
A[i] = A[j];
A[j] = temp;
}
}
}
// 第一轮循环完
// 8 2 5 7 6 10 9 1 4 3
// 第二轮循环完
// 8 2 10 7 6 5 9 1 4 3
// 第三轮循环完
// 8 9 10 7 6 5 2 1 4 3
// 第四轮循环完
// 跳出
int main() {
int a[10] = {3, 2, 5, 7, 6, 10, 9, 1, 4, 8}; // 初始化整数数组a
int n = 10; // 数组长度
for (int i = 0; i < n; i++)
printf("%d ", a[i]); // 打印初始数组元素
printf("\n");
NewSequence(a, n); // 调用函数对数组重新安排
for (int i = 0; i < n; i++)
printf("%d ", a[i]); // 打印重新安排后的数组元素
// 8 9 10 7 6 5 2 1 4 3
printf("\n");
return 0;
}
代码思路解释:
- 初始化两个指针i和j,分别指向数组的起始位置和末尾位置。
- 进入循环,循环条件是i小于数组长度n且j大于等于0,即两个指针都在数组范围内。
- 在循环体中,首先在数组中找到第一个奇数元素的位置,通过A[i] % 2 == 0判断元素是否为偶数,如果是偶数则继续向后遍历,直到找到第一个奇数元素的位置。
- 然后在数组中找到第一个偶数元素的位置,通过A[j] % 2 == 1判断元素是否为奇数,如果是奇数则继续向前遍历,直到找到第一个偶数元素的位置。
- 如果找到了奇数元素和偶数元素的位置,交换这两个元素。
- 继续循环,直到遍历完整个数组。
- 最终,奇数元素会存放在奇数单元,偶数元素会存放在偶数单元。
- 这个算法的时间复杂度为O(n),因为需要遍历整个数组。空间复杂度为O(1),因为只需要常数级别的额外空间来存储变量。
5.(2017)将n个正整数存放于一个一维数组A中,试设计一个算法,将所有的奇数移动并存放于数组的前半部分,将所有的偶数移动并存放于数组的后半部分,要求尽可能少地使用存储单元,并使计算时间达到O(n)
void sort(int A[], int n) {
int low = 0, high = n - 1; // 初始化低位指针和高位指针
int temp = A[low]; // 保存低位元素的值
while (low < high) { // 循环直到低位指针和高位指针相遇
while (low < high && A[high] % 2 == 0) // 从高位开始找到第一个奇数元素
high--;
A[low] = A[high]; // 将奇数元素移到低位
while (low < high && A[low] % 2 == 1) // 从低位开始找到第一个偶数元素
low++;
A[high] = A[low]; // 将偶数元素移到高位
}
A[high] = temp; // 将原来的低位元素放回数组
}
int main() {
int A[] = {3, 2, 5, 7, 6, 10, 9, 1, 4, 8}; // 初始化数组
int n = sizeof(A) / sizeof(A[0]); // 数组长度
printf("Original Array:\n");
for (int i = 0; i < n; i++)
printf("%d ", A[i]); // 打印初始数组元素
sort(A, n); // 调用函数对数组进行奇偶分割
printf("\nSorted Array:\n");
for (int i = 0; i < n; i++)
printf("%d ", A[i]); // 打印重新安排后的数组元素
printf("\n");
return 0;
}
Original Array:
3 2 5 7 6 10 9 1 4 8
Sorted Array:
1 9 5 7 3 10 6 2 4 8
代码的思路是使用两个指针low和high,分别指向数组的低位和高位。然后,通过两个嵌套的循环,分别找到第一个奇数元素和第一个偶数元素的位置,并将它们互换位置。循环过程中,指针low从低位开始向高位移动,指针high从高位开始向低位移动,以便继续寻找下一个奇数和偶数元素。最后,将原来的低位元素放回数组的最后一个位置,以保证奇数在前半部分,偶数在后半部分。
这段代码的时间复杂度为O(n),因为每个元素最多被访问两次,且只进行了常数次的交换操作。同时,代码只使用了常数个额外的存储单元,满足题目的要求。
6.(不知道哪一年 没懂 )已知带表头线性表header,元素在10000以上,元素类型为整型。设计算法对该链表进行筛选,只保留其中的前100个最小的元素。
修改后的BubbleSort函数使用的是冒泡排序算法。
冒泡排序算法的基本思想是通过相邻元素的比较和交换来实现排序。具体步骤如下:
- 外层循环控制比较的轮数,总共需要进行count-1轮比较,其中count为链表的结点个数。
- 内层循环控制每轮比较的次数,每轮比较的次数为当前需要比较的结点对数。
- 在内层循环中,比较相邻的两个结点的值,如果前一个结点的值大于后一个结点的值,则交换它们的位置。
- 交换操作将较大的值向后移动,因此每轮比较结束后,最大的元素会被移动到链表的尾部。
- 内层循环结束后,继续进行下一轮比较,直到所有的元素都按照从小到大的顺序排列。
冒泡排序算法的时间复杂度为O(n^2),其中n为链表的结点个数。由于每一轮内层循环都会将一个最大的元素移动到尾部,因此在最好的情况下,当链表已经有序时,算法的时间复杂度可以达到O(n)。
#include <iostream>
struct NODE {
int data;
NODE* next;
};
void BubbleSort(NODE *&L) {
int i, count = 0, num; // 定义变量i、count、num
NODE *p, *q, *tail; // 定义指针p、q、tail
p = L; // 将p指向链表的头结点
while (p->next != NULL) { // 遍历链表,计算链表的结点个数
count++; // 结点个数加1
p = p->next; // 移动指针p到下一个结点
}
for (i = 0; i < count - 1; i++) { // 外层循环,控制比较的轮数
num = count - i - 1; // 记录内层循环需要的次数
q = L->next; // 将q指向链表的第一个结点
p = q->next; // 将p指向q的下一个结点
tail = L; // 将tail指向q前一个结点,方便交换和进行下一步操作
while (num--) { // 内层循环,控制每轮比较的次数
if (q->data > p->data) { // 如果当前结点的值大于后一个结点的值,则交换它们的位置
q->next = p->next;
p->next = q;
tail->next = p;
}
tail = tail->next; // 移动tail指针到下一个结点
q = tail->next; // 移动q指针到下一个结点
p = q->next; // 移动p指针到下一个结点
}
}
}
int main() {
// 创建一个带表头的线性链表
NODE* header = new NODE;
header->next = nullptr;
NODE* current = header;
// 向链表中添加元素(假设元素大于10000)
for (int i = 1; i <= 100000; i++) {
NODE* newNode = new NODE;
newNode->data = i;
newNode->next = nullptr;
current->next = newNode;
current = newNode;
}
// 对链表进行筛选,保留前100个最小的元素
BubbleSort(header);
// 输出筛选后的结果
NODE* p = header->next;
for (int i = 0; i < 100 && p != nullptr; i++) {
std::cout << p->data << " ";
p = p->next;
}
std::cout << std::endl;
// 释放链表内存
p = header;
while (p != nullptr) {
NODE* temp = p;
p = p->next;
delete temp;
}
return 0;
}
解析代码思路:
- 首先,通过遍历链表计算链表的结点个数,将结点个数存储在变量count中。
- 使用外层循环,控制比较的轮数,总共需要进行count-1轮比较。
- 在外层循环中,记录内层循环所需的次数,即将要进行比较的结点对数,存储在变量num中。
- 初始化指针q为链表的第一个结点,指针p为q的下一个结点,指针tail为q前一个结点。
- 在内层循环中,进行相邻结点的值比较和交换操作。
- 如果当前结点q的值大于后一个结点p的值,则交换它们的位置。
- 更新指针tail,将其指向下一个结点。
- 更新指针q,将其指向tail的下一个结点。
- 更新指针p,将其指向q的下一个结点。
- 内层循环结束后,一轮比较完成,继续进行下一轮比较。
- 外层循环结束后,链表中的结点按照从小到大的顺序排列。
选择排序,每次选一个
7.(04年真题? 没懂 )设有一个双向链表,每个结点中除有 prior(指向其前导结点)、data(数据域)和 next (指向其后继结点)三个域外,还有一个访问频度域 freq,在链表被起用之前,其值均初始化为零。每当在链表进行一次 LocateNode(L,x)运算时, 令元素值为 x 的结点中 freq 域的值加 1,并调整表中结点的次序,使其按访问频度的递减序排列,以便使频繁访问的结点总是靠近表头。试写一符合上述要求的 LocateNode 运算的算法。
【算法思想】在 DLinkList 类型的定义中添加 int freq 域,将所有节点的 freq 域均初始化为 0。在查找到 data 域为 x 的节点p 时,将其 freq 域增 1。再找到p节点的前驱节点pre,若pre 不是头节点,且满足 pre->freq < p->freq,则 pre 指针再向前移,直到找到一个节点pre,满足 pre->freq≥p->freq,则将p 节点移到pre 节点之后,如图所示,其操作是先删除p 节点,再将其插入到pre 节点之后。
#include <iostream>
typedef int ElemType;
// 双向链表结点的定义
typedef struct DuLNode {
ElemType data;
int freq; // 访问频度
struct DuLNode *prior, *next; // 前驱和后继指针
} DuLNode, *DuLinkList;
// 在链表中定位结点并更新频度
int LocateNode(DuLinkList &h, ElemType x);
int main() {
DuLinkList head = new DuLNode; // 创建头结点
head->next = NULL;
head->prior = NULL;
head->freq = 0;
// 创建链表
DuLinkList node1 = new DuLNode;
node1->data = 1;
node1->freq = 0;
node1->prior = head;
head->next = node1;
DuLinkList node2 = new DuLNode;
node2->data = 2;
node2->freq = 0;
node2->prior = node1;
node1->next = node2;
DuLinkList node3 = new DuLNode;
node3->data = 3;
node3->freq = 0;
node3->prior = node2;
node2->next = node3;
node3->next = NULL;
// 测试 LocateNode 函数
int result = LocateNode(head, 2);
if (result == 1) {
std::cout << "找到并更新了结点" << std::endl;
} else {
std::cout << "未找到结点" << std::endl;
}
// 输出链表中每个结点的值和频度
DuLinkList p = head->next;
while (p != NULL) {
std::cout << "结点值:" << p->data << ",频度:" << p->freq << std::endl;
p = p->next;
}
// 释放链表内存
p = head;
while (p != NULL) {
DuLinkList temp = p;
p = p->next;
delete temp;
}
return 0;
}
// 找到并更新了结点
// 结点值:2,频度:1
// 结点值:1,频度:0
// 结点值:3,频度:0
// 在链表中定位结点并更新频度
int LocateNode(DuLinkList &h, ElemType x) {
DuLinkList p = h->next; // 从头结点的下一个结点开始遍历链表
DuLinkList q;
while (p != NULL && p->data != x) // 遍历链表,直到找到值为 x 的结点或者链表遍历结束
p = p->next;
if (p == NULL) // 如果没有找到值为 x 的结点
return 0;
else {
p->freq++; // 找到了值为 x 的结点,将其频度加1
q = p->prior; // q 为 p 的前驱结点
if (q != h) { // 如果 p 不是第一个数据结点
while (q != h && q->freq < p->freq) // 找到 q 结点,使得 q 的频度大于等于 p 的频度
q = q->prior;
p->prior->next = p->next; // 先删除 p 结点
if (p->next != NULL)
p->next->prior = p->prior;
p->next = q->next; // 将 p 结点插入到 q 结点之后
if (q->next != NULL)
q->next->prior = p;
q->next = p;
p->prior = q;
}
return 1;
}
}
代码思路解释:
- 首先,定义两个指针变量 p 和 q,分别用来遍历链表和定位插入位置。
- 使用 p 指针遍历链表,直到找到值为 x 的结点或者链表遍历结束。
- 如果没有找到值为 x 的结点,返回 0 表示未找到。
- 如果找到了值为 x 的结点,将其频度加1。
- 将 q 指针指向 p 的前驱结点。
- 如果 q 不是头结点,就在链表中找到一个结点 q,使得 q 的频度大于等于 p 的频度。
- 删除结点 p,将其插入到结点 q 之后。
- 返回 1 表示成功找到并更新了结点。
时间复杂度:在最坏情况下,需要遍历整个链表,时间复杂度为 O(n),其中 n 是链表的长度。
空间复杂度:除了输入参数外,算法的空间复杂度为 O(1),即常数级别的额外空间使用。
int LocateNode(DLinkList * L, ElmeType x) {
DLinkList * p = L - > next, * pre;
while (p != NULL && p - > data != x)
p = p - > next; //找到 data 域值为 x 的结点 p;
if (p == NULL) //如果没有找到,返回
return 0;
else { //找到这样的结点
p - > freq++; //频度 freq +1
pre = p - > prior; //pre 为 p 的前驱结点
if (pre != L) {
while (pre != L && pre - > freq < p - > freq) //找到 pre 结点
pre = pre - > prior;
p - > prior - > next = p - > next; //删除结点 p
if (p - > next != NULL)
p - > next - > prior = p - > prior;
p - > next = pre - > next; //将结点 p 插入到 pre 结点之后
if (pre - > next != NULL)
pre - > next - > prior = p;
pre - > next = p;
p - > prior = pre;
}
return 1;
}
}
8. 求两个表的交集,链式存储
方法一:遍历
方法二:先排序再遍历
【算法思想】
A 与 B 的交集是指同时出现在两个集合中的元素,因此,此题的关键点在于:依次摘取两个表中相等的元素重新进行链接,删除其他不等的元素。
算法思想是:假设待合并的链表为 La 和 Lb,合并后的新表使用头指针 Lc(Lc 的表头结点设为 La 的表头结点)指向。pa 和 pb 分别是链表 La 和 Lb 的工作指针,初始化为相应链表的首元结点。从首元结点开始进行比较,当两个链表 La 和 Lb均为到达表尾结点时,如果两个表中的元素相等,摘取 La 表中的元素,删除 Lb表中的元素;如果其中一个表中的元素较小,删除此表中较小的元素,此表的工作指针后移。当链表 La 和 Lb 有一个到达表尾结点为空时,依次删除另一个非空表中的所有元素。最后释放链表 Lb 的头结点。
void Intersection(LinkList &La, LinkList &Lb, LinkList &Lc) {
// 初始化工作指针和结果链表
pa = La->next; // pa 是链表 La 的工作指针,初始化为首元结点
pb = Lb->next; // pb 是链表 Lb 的工作指针,初始化为首元结点
Lc = pc = La; // 用 La 的头结点作为 Lc 的头结点
// 遍历链表 La 和 Lb
while (pa && pb) {
if (pa->data == pb->data) {
// 相等,交集并入结果表中
pc->next = pa;
pc = pa;
pa = pa->next;
u = pb;
pb = pb->next;
free(u); // 释放链表 Lb 中的节点
} else if (pa->data < pb->data) {
// 删除较小者 La 中的元素
u = pa;
pa = pa->next;
free(u); // 释放链表 La 中的节点
} else {
// 删除较小者 Lb 中的元素
u = pb;
pb = pb->next;
free(u); // 释放链表 Lb 中的节点
}
}
// 处理剩余的节点
while (pa) {
// Lb 为空,删除非空表 La 中的所有元素
u = pa;
pa = pa->next;
free(u); // 释放链表 La 中的节点
}
while (pb) {
// La 为空,删除非空表 Lb 中的所有元素
u = pb;
pb = pb->next;
free(u); // 释放链表 Lb 中的节点
}
pc->next = NULL; // 设置结果链表尾节点的 next 指针为 NULL
free(Lb); // 释放链表 Lb 的头结点
}
代码的思路如下:
- 初始化工作指针和结果链表。
- 遍历链表 La 和 Lb,比较当前节点的值。
- 如果节点值相等,则将该节点加入结果链表 Lc,并继续遍历下一个节点。
- 如果节点值不相等,则删除较小值的节点,并继续遍历下一个节点。
- 处理剩余的节点,如果链表 La 还有剩余节点,则删除它们;如果链表 Lb 还有剩余节点,则删除它们。
- 设置结果链表 Lc 的尾节点的 next 指针为 NULL,表示链表结束。
- 释放链表 Lb 的头结点。
这段代码的目的是找到链表 La 和链表 Lb 的交集,并将结果存储在链表 Lc 中。
9.(2015真)设 H1、H2为两个链表的头指针,编写算法Judge 判断两个单链表是否有交叉,要求效率尽量高,并分析时间空间复杂度。
方法一:先找出两者的长度,长的链表先多走几步再同时走
算法设计思想:可以知道如果两个链表有公共节点,那么该公共节点之后的所有节点都是两个链表所共有的,所以长度一定也是相等的,如果两个连表的总长的是相等的,那么我们对两个连表进行遍历,则一定同时到达第一个公共节点。但是连表的长度实际不一定相同,所以我们只需要计算出来年各个链表的长度之差n,然后让长的那个表先移动n,短的那个表在开始遍历,这样它们一定同时到达打一个公共节点,我们只需要在向后移动的时候比较两个链表的节点是否相等就可以获得第一个公共节点。
判断两个链表是否交叉。如果交叉,则返回h1链表的交叉点;否则,返回NULL。
参数h1为第一个链表的头结点,h2为第二个链表的头结点。
// 数据结构定义
typedef struct LNode {
ElemType data; // 节点数据
struct LNode* next; // 指向下一个节点的指针
} LNode, *LinkList;
LinkList Judge(LinkList h1, LinkList h2) {
LinkList tp1 = h1, tp2 = h2; // 创建两个临时指针,用于遍历链表
int NodeNum1 = 0, NodeNum2 = 0; // 用于记录链表的节点个数
if (h1 == NULL || h2 == NULL) // 判断头指针是否为空
return NULL;
// 统计链表 1 的节点个数
while (tp1->next != NULL) {
NodeNum1++;
tp1 = tp1->next;
}
// 统计链表 2 的节点个数
while (tp2->next != NULL) {
NodeNum2++;
tp2 = tp2->next;
}
// 如果两个链表的最后一个节点不一样,说明两个链表无交叉结点
if (tp1 != tp2)
return NULL;
else { // 两链表有交叉点
int i;
tp1 = h1->next;
tp2 = h2->next;
if (NodeNum1 < NodeNum2) {
// tp1, NodeNum1中保存较长的链表信息
tp1 = h2->next;
tp2 = h1->next;
i = NodeNum1;
NodeNum1 = NodeNum2;
NodeNum2 = i;
}
for (i = 0; i < NodeNum1 - NodeNum2; i++)
tp1 = tp1->next;
while (tp1->next != NULL) {
if (tp1 == tp2)
return tp1; // 返回交叉节点的指针
else {
tp1 = tp1->next;
tp2 = tp2->next;
}
}
}
return NULL; // 返回结果,根据实际情况修改
}
- 时间复杂度:遍历链表 1 和链表 2 分别统计节点个数的时间复杂度为 O(n),其中 n 分别为链表 1 和链表 2 的节点个数。后续的操作包括节点个数的比较、节点的遍历,时间复杂度为 O(max(m, n)),其中 m 和 n 分别为链表 1 和链表 2 的节点个数。因此,总体时间复杂度为 O(max(m, n))。
- 空间复杂度:除了存储链表节点数据和指针的空间外,额外使用了常数个变量,因此空间复杂度为 O(1)。
10.(模拟)(剑指offer-JZ57)输入一个正数s,打印出所有和为s的连续正数序列(至少含有两个数)。例如输入15,由于1+2+3+4+5=4+5+6=7+8=15,所以结果打印出3个连续序列15、4~6和78
#include <iostream>
void PrintContinuousSequence(int small, int big); // 声明打印连续序列的函数
void FindContinuousSequence(int sum)
{
if(sum < 3) // 如果给定的和小于3,不存在连续序列,直接返回
return;
int small = 1; // 初始化连续序列的起始值为1
int big = 2; // 初始化连续序列的结束值为2
int middle = (1 + sum) / 2; // 连续序列的中间值,用于判断循环终止条件
int curSum = small + big; // 当前连续序列的和
while(small < middle) // 当起始值小于中间值时进行循环
{
if(curSum == sum) // 如果当前连续序列的和等于给定的和,打印该序列
PrintContinuousSequence(small, big);
while(curSum > sum && small < middle) // 如果当前连续序列的和大于给定的和,移动起始值
{
curSum -= small; // 减去起始值
small ++; // 起始值右移
if(curSum == sum) // 如果移动后的连续序列的和等于给定的和,打印该序列
PrintContinuousSequence(small, big);
}
big ++; // 结束值右移
curSum += big; // 加上新的结束值
}
}
void PrintContinuousSequence(int small, int big)
{
for(int i = small; i <= big; ++ i) // 循环打印连续序列
std::cout << i << " ";
std::cout << std::endl;
}
// ====================测试代码====================
void Test(const char* testName, int sum)
{
if(testName != nullptr)
std::cout << testName << " for " << sum << " begins: " << std::endl;
FindContinuousSequence(sum); // 调用函数查找和为给定值的连续序列
}
int main(int argc, char* argv[])
{
Test("test1", 1);
Test("test2", 3);
Test("test3", 4);
Test("test4", 9);
Test("test5", 15);
Test("test6", 100);
return 0;
}
11.(2021真)
在一个长度为n的数组里,所有元素都是0~n-1范围内的整数。某些元素在数组中可能重复出现,但不知道哪些是重复出现的,也不知道重复出现多少次。现要尽可能快地找出数组中所有重复出现的元素。请回答下列问题:
1)设计相关的数据结构。
2)描述求解问题的方法步骤,并说明时间和空间效率
法一:代码思路解释:
- 遍历数组中的每个元素,从第一个元素到倒数第二个元素。
- 对于当前元素,从其下一个元素开始,与后面的元素进行比较。
- 如果找到相同的元素,即为重复出现的数字,输出该数字。
这段代码的时间复杂度为O(n^2),因为它使用了两个嵌套的循环。如果数组长度较大,性能可能会较低。
#include <stdio.h>
void find_same_number(int arr[], int len) {
int i, j;
for (j = 0; j < len - 1; j++) { // 遍历数组,从第一个元素到倒数第二个元素
for (i = j + 1; i < len; i++) { // 从当前元素的下一个元素开始,与后面的元素进行比较
if (arr[j] == arr[i]) {
printf("重复出现的数字:%d\n", arr[j]);
// 如果想要在找到重复元素后立即返回,可以使用 return 语句
// return;
}
}
}
}
int main(int argc, char const *argv[]) {
int arr[7] = {2, 3, 1, 0, 2, 5, 3};
int len = sizeof(arr) / sizeof(arr[0]); // 计算数组长度
find_same_number(arr, len);
return 0;
}
法二:代码思路解释:
- 定义一个大小为N的数组a,用于记录每个数字出现的次数。数组下标表示数字本身,数组元素表示该数字出现的次数。
- 遍历长度为n的数组num。
- 对于当前数字num[i],如果a[num[i]]为0,说明该数字还未出现过,将a[num[i]]加1,表示该数字出现了一次。
- 如果a[num[i]]大于等于1,说明该数字已经出现过,即为重复出现的数字,输出该数字。
这段代码使用数组a记录每个数字出现的次数,时间复杂度为O(n),空间复杂度为O(N)。
#include <stdio.h>
#define N 1000
void find_duplicates(int num[], int n) {
int a[N] = {0}; // 初始化数组a,用于记录每个数字出现的次数
for (int i = 0; i < n; i++) {
if (!a[num[i]]) { // 如果a[num[i]]为0,说明该数字还未出现过
a[num[i]]++; // 将a[num[i]]加1,表示该数字出现了一次
} else if (a[num[i]] >= 1) { // 如果a[num[i]]大于等于1,说明该数字已经出现过
printf("重复出现的数字:%d\n", num[i]);
}
}
}
int main() {
int num[7] = {2, 3, 1, 0, 2, 5, 3};
int n = sizeof(num) / sizeof(num[0]); // 计算数组长度
find_duplicates(num, n);
return 0;
}
法三:代码思路解释:
- 创建一个长度为n的数组b,用于记录数字出现的次数。
- 初始化数组b,将全部元素置为0。
- 遍历已存在的数组num。
- 将数字num[i]作为下标,将对应位置的元素加1。
- 遍历数组b,如果b[i]大于1,说明数字i重复出现,输出该数字。
这段代码的时间复杂度为O(n),空间复杂度为O(n)。相比之前的代码,它使用了一个辅助数组b来记录数字出现的次数,通过空间换取了更快的速度。
#include <stdio.h>
void find_duplicates(int num[], int n) {
int b[n]; // 创建长度为n的数组b,用于记录数字出现的次数
for (int i = 0; i < n; i++) {
b[i] = 0; // 初始化数组b,全部置为0
}
for (int i = 0; i < n; i++) {
b[num[i]]++; // 将数字num[i]作为下标,将对应位置的元素加1
}
for (int i = 0; i < n; i++) {
if (b[i] > 1) { // 如果b[i]大于1,说明数字i重复出现
printf("重复出现的数字:%d\n", i);
}
}
}
int main() {
int num[7] = {2, 3, 1, 0, 2, 5, 3};
int n = sizeof(num) / sizeof(num[0]); // 计算数组长度
find_duplicates(num, n);
return 0;
}
21年的第二题(大概是树)
struct ListNode {
int data; // 节点存储的值
ListNode* left; // 左子节点指针
ListNode* right; // 右子节点指针
};
// 查找小于目标节点值的最大节点
ListNode* search(ListNode* cur, ListNode* p) {
ListNode* max = NULL; // 初始化最大节点指针为空
int num = p->data; // 获取目标节点的值,存储在变量 num 中
while (cur != NULL) { // 循环遍历链表,直到当前节点为空
if (cur->data < num) { // 如果当前节点的值小于目标节点的值
max = cur; // 更新最大节点指针为当前节点
cur = cur->right; // 向右子节点移动
} else {
cur = cur->left; // 如果当前节点的值大于等于目标节点的值,则向左子节点移动
}
}
return max; // 返回小于目标节点值的最大节点指针
}