一、C:
1.static和const的作用优缺点
限制作用域:
static声明中使用全局变量、函数 ,仅当前文件内可用,其他文件不能引用
static修饰的局部变量只能在本函数中使用。
延长生命周期:
static修饰的变量生命周期为整个程序
存放位置:
static修饰的变量存放在静态区
初始化:
static变量未赋初值时初值为0,且只初始化一次
const 是常量化的意思;
可以修饰变量,可以修饰指针。
当修饰变量的时候,因为不可以通过变量名对变量的值进行修改所以在定义变量的时候需要给变量初始化;
当修饰指针的时候,const位置不同,修饰的指针的指向或内容不能改变。
char *const a 指向不可修改,内容可以修改
char const *a 内容不可以修改,指向可以修改
const char *a 内容不可以修改,指向可以修改
- typedef和define的区别
1.处理时机:
typedef:typedef 创建类型别名,编译器在编译时会对其进行处理。
#define:创建宏定义,预处理器在预处理阶段会对其进行处理。即在编译之前,预处理器会将代码中的宏定义替换为相应的内容。
3.作用范围:
typedef:typedef 定义的类型别名在作用域内有效,并且可以在多个函数或文件中使用。
#define:#define 定义的宏定义在定义位置后全局有效,可以在整个代码中使用,直到遇到 #undef 指令或代码结束。
4.语法格式:
typedef:typedef 的语法格式为:typedef <existing_type>
<new_type>;
#define:#define 的语法 <macro_name> <replacement>
5.类型安全:
typedef:typedef 创建的别名是类型安全的,因为它们实际上是原类型的别名。
#define:#define 创建的宏定义不进行类型检查,它仅仅是简单的文本替换。
6.适用场景:
typedef:typedef 通常用于创建复杂类型的别名,使代码更易读和可维护。
#define:#define 主要用于创建简单的常量或函数宏,用于简化代码或定义一些特定的标识符。
枚举(enum)
枚举是一种用户自定义的数据类型,它为一组相关的整数常量赋予了有意义的名称,枚举里成员系统会自动赋初值,第一个成员为0,依次类推;如果程序员想赋值的,假设第一个成员赋值为1,那么第二个成员系统会赋值为2,依次类推
- volatile作用、Extern作用
volatile 主要有以下作用:
volatile是一个关键字,用于在 C 和 C++ 中修饰变量,它主要用于告诉编译器该变量的值可能在程序执行期间发生变化,从而禁止编译器对该变量进行优化。
- 告知编译器不要优化: 优化代码时,会对变量进行优化,如寄存器优化、常量传播等,以提高执行效率。但对于被 volatile 修饰的变量,编译器会认为其值可能在程序执行期间被意外地更改,因此不会进行优化,确保变量的值每次都从内存中读取。
- 防止可能会存在多个线程或处理器同时访问同一个变量。如果该变量不使用 volatile 修饰,编译器可能会将其缓存到寄存器或者优化编译器缓存变量值: 在多线程或中断处理程序中,为常量,这样可能导致线程间不同步或者中断处理程序无法正确获取最新的变量值。而使用 volatile 修饰后,编译器每次都会从内存中读取变量的值,确保变量的值始终是最新的。
- 与外部设备的交互: 当程序与外部设备进行交互时,如硬件寄存器或存储映射 I/O 等,外部设备可能会在任何时候更改这些变量的值。在这种情况下,使用 volatile 修饰这些变量可以确保程序正确读取设备的状态,而不会受到编译器的优化干扰。
Extern
4.sizeof和strlen区别
strlen是函数,用于计算字符串的长度;不包含`\0`;
sizeof是关键字,用于计算变量、数组、其他数据类型所占内存空间的大小;当sizeof计算字符串长度的时候,包含`\0`;
- 数组和链表的区别
数组
数组是一个相同类型的数据集合
数组元素可以使用数组索引随机访问
数组的数据元素在内存中连续存储
插入和删除非常耗时,时间为O(n)
数组的内存是静态分配的,在编译期间完成
链表
链表是一个有相同数据类型的有序集合,其中每个元素使用指针链接
链表不允许随机访问,链表创建一个指针指向相应的数据
链表的插入和删除非常快,时间为O(1)
链表的内存分配-是动态的,在运行时动态分配
链表的大小随元素的插入或删除动态变化
链表和内核链表的区别:
- 内核链表和链表的区别:“内核链表”特指在操作系统内核环境中使用的链表,它可能有一些针对操作系统需求的特殊设计或优化;
- 而“链表”则是一个更通用的概念,可以应用于任何软件开发场景。
- 内核链表通常是由操作系统内核提供的,并且有一套封装好的函数来支持其操作。
- 当我们在用户空间的程序中使用链表时,通常需要自己定义链表的数据结构以及相关的操作函数。
6.指针相关
指针就是地址,指针变量就是存放地址的变量;指针可以使用简单的运算符操作;指针加一或者自加,代表指向下一个元素;对于32位系统,指针占4字节
数组是同名类型是数组数据的集合,内存连续。数组的首地址,是地址常量,不可以进行自加等操作;
指针数组的本质是数组,数组里存放的是指针。int *p[3]
数组指针的本质是指针,指向数组的指针称为数组指 针。int (*p)[3]可以间接访问二维数组。
函数指针本质是指针,指向函数的指针 ,一般用做函数的参数,实现代码复用,也可以作为结构体成员,指向某个函数。 int (*p)(int,int)
7.结构体和共用体区别
二者都是构造数据类型
1)结构体:让C语言实现面向对象的思想。结构体使用的时候,结构体中每一个成员都有自己的内存空间,计算结构体大小的时候要注意内部字节对齐;
2)共用体又叫联用体,大小等于成员中占内存最大的那个大小。合体,每一个成员都共享内存空间。因此共用体大小等于成员中占内存最大的那个大小。
结构体 (struct)
- 内存分配:每个成员变量都有其自己的内存空间。
- 内存是连续分配的,但可能由于对齐规则导致有填充字节。
- 访问成员:可以同时访问所有成员。
- 大小:结构体的总大小等于所有成员所占空间之和加上任何对齐填充的空间。
结构体的大小并不是简单的将每个成员的大小相加就能得到的
sizeof(struct 结构体名); // 结构体类型的大小
结构体内存对齐规则
- 第一个成员在结构体变量偏移量为0的地址处。(及结构体的首地址处)
- 个数字(对齐数)的整其他成员变量要对齐到某数倍
- 如果嵌套了结构体,嵌套的结构体对齐到自己的最大整数的倍数处,结构体整体的大小就是所有最大对齐数的整数倍
对齐数:该结构体成员变量自身的大小与编译器默认(64位是8、32位是4)的一个大小比较,取小值
- 用途:
- 用于组合不同类型的变量以形成一个复合数据类型。
- 常用于表示复杂的数据结构,例如一个人的信息包含姓名、年龄和身高。
共用体 (union)
- 内存分配:所有成员共享同一段内存空间。
- 访问成员:不能同时访问所有成员,因为它们共享相同的内存位置。
- 当改变一个成员的值时,会影响其他成员的值。
- 大小:共9
- 用体的大小等于最长的成员所占的空间。
- 用途:
- 用于节省内存,当多个数据成员不可能同时使用时。
- 常用于实现动态数据类型,如变长数组或实现多态性。
结构体大小计算(字节对齐)
#pragma 是一个预处理指令,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作,#pragma pack 的 主要作用就是改变编译器的内存对齐方式
(1)结构体各个成员变量的首地址必须是 min{自身对齐值,指定对齐值的整数倍。
(2)结构体各个成员相对于结构体起始地址的偏移量(offset)是 min{该成员数据类型大小,指定对齐值} 的整数倍,如有需要,编译器会在成员之间加上填充字节。
(3)结构体分配的总空间大小必须是 min {其最宽基本数据类型成员,指定对齐值} 的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。
- 堆和栈的区别
栈编译器自动管理,无需程序员手工控制;
堆空间的申请释放工作 由程序员控制,通过 malloc/free申请释放空间,容易产生内存泄漏。
(2)空间大小不同。
栈是向低地址扩展的数据结构,是一块连续的内存区域。
堆是向高地址扩展的数据结构,是不连续的内存区域。
(3)是否产生碎片。
对于堆来讲,频繁的malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低(虽然程序在退出后操作系统会对内存进行回收管理)。对于栈来讲,则不会存在这个问题。
(4)增长方向不同。
堆的增长方向是向上的,即向着内存地址增加的方向;栈的增长方向是向下的,即向着内存地址减小的方向。
栈区存放局部变量,函数形参,函数返回值。
用户空间划分:
代码段(Text Segment):存放程序的机器指令。
数据段(Data Segment):存放已初始化的全局变量和静态变量。
BSS段(BSS Segment):存放未初始化的全局变量和静态变量。
堆(Heap):动态分配的内存区域,如使用malloc或new分配的内存。
栈(Stack):用于局部变量、形参、返回值存储。
- 全局变量和局部变量的区别
定义:全局变量是定义在函数外部的变量,局部变量是定义在函数内部的变量
存储位置:全局变量存储在全局区,局部变量存储在栈区
作用域:全局变量可以在程序任意位置使用,局部变量只能在函数内部使用
生命周期:全局变量的生命周期为整个程序,程序结束空间释放,局部变量生命周期为本函数,函数结束空间释放
初始化:全局变量未初始化初值为0,局部变量未初始化时值为随机值
11.memcpy和strcpy的区别?
- 是用于拷贝指定大小的数据块(字节),它不会自动检测字符串结束符('\0'),因此适用于拷贝任意数据,包括字符串和非字符串数据。
- 是用于拷贝第一个以 '\0' 结尾的字符串,它会自动拷贝整个字符串,包括字符串结束符 '\0'。
- 的函数原型为 void *memcpy(void *dest, const void *src, size_t n);,其中 dest 是目标地址,src 是源地址,n 是拷贝的字节数。
- 的函数原型为 char *strcpy(char *dest, const char *src);,其中 dest 是目标字符串的地址,src 是源字符串的地址。
- 安全性:
- 不会检查目标地址是否足够大,如果目标地址空间不够大,可能会发生缓冲区溢出的问题,导致程序崩溃或数据损坏。因此,在使用 memcpy 时需要确保目标地址有足够的空间来容纳源数据。
- 会自动添加符字串结束符 '\0',但如果源字符串太长,超出了目标字符串的空间,也会导致缓冲区溢出问题。因此,使用 strcpy 时应留足够的空间来·容纳源字符串。
- 什么是段错误?怎么解决段错误?
当程序尝试访问它没有权限访问的内存地址时
段错误通常是由以下几种情况引起的:
- 访问空指针: 当程序试图访问一个空指针所指向的内存地址时,就会发生段错误。例如,未初始化的指针或者指向已释放的内存的指针。
- 数组越界: 当程序试图访问数组越界的元素时,就会发生段错误。例如,访问数组下标小于 0 或者大于等于数组大小的元素。
- 非法内存访问: 当程序试图访问未分配的内存区域,或者试图访问系统保留的内存区域,就会发生段错误。
- 对只读内存的写操作:当程序试图对只读内存进行写操作时,例如对常量字符串进行修改,就会发生段错误。
要解决段错误问题,可以通过以下几种方式:
- 检查空指针: 在使用指针之前,先确保指针m不是空指针。可以使用条件语句或者断言来检查指针是否为空。
- 数组边界检查: 在访问数组元素之前,先确保访问的下标在合法的范 围内。
- 动态内存管理: 在动态分配内存(例如使用 malloc、calloc、new m等函数)后,要确保在不再使用该内存时释放它(例如使用 free、 delete 等函数)。
- 避免对只读内存进行写操作: 对于常量字符串或者是只读内存区域,避免对其进行写操作,可以使用 const 关键字来声明指向只读内存的 指针。
- 使用工具检查: 可以使用一些工具,如 Valgrind,在程序运行时检查内存错误,包括段错误、内存泄漏等。
13.什么是内存泄漏?什么是野指针?
内存泄漏(Memory Leak): 内存泄漏是指程序在动态分配内存后,未释放不再使用的内存,导致这部分内存永远无法被回收,从而造成内存的浪费。如果程序中存在内存泄漏,随着程序的执行,内存占用会逐渐增加,最终可能导致程序崩溃或系统资源耗尽。内存泄漏的常见原因包括:
- 忘记释放通过 malloc、calloc、new 等动态分配的内存。
- 在循环或迭代过程中,每次分配内存却未及时释放导致累积。
- 保存了指向动态分配内存的指针,但在后续程序中指针又指向其他位置从而丢失了释放的机会。
野指针(Wild Pointer): 野指针是指指向未知或者无效内存地址的指针。野指针通常产生于以下几种情况:
- 指针未初始化:指针没有初始化,即没有赋予合法的地址,指针的初始值是随机的。
- 内存释放后未置空:指针指向的内存已经被释放,但指针没有置为 nullptr 或 NULL。
- 指针越界访问:指针指向的内存已经被释放,但程序继续使用该指针。
编程题:
1.define比较两个数的大小
#define MIN(a, b) ((a) < (b)?(a) : (b))
- 将字符串逆序输出
void reverseString(char *str) {
int length = strlen(str);
char *start = str;
char *end = str + length - 1;
while (start < end) {
char temp = *start;
*start = *end;
*end = temp;
start++;
end--;
}
}
int main() {
char str[] = "Hello, World!";
printf("原始字符串:%s\n", str);
reverseString(str);
printf("逆序输出:%s\n", str);
return 0;
}
3.冒泡排序
void bubbleSort(int arr[], int n) {
int i, j;
for (i = 0; i < n-1; i++) {
// 每次循环将最大的元素冒泡到末尾,因此每轮循环只需比较前 n-i-1 个元素
for (j = 0; j < n-i-1; j++) {
if (arr[j] > arr[j+1]) {
// 如果前一个元素大于后一个元素,则交换它们的位置
int temp = arr[j]; //arr[j] ^= arr[j + 1];
arr[j] = arr[j+1]; //arr[j + 1] ^= arr[j];
arr[j+1] = temp; //arr[j] ^= arr[j + 1];
}
}
}
}
4.strlen、strcpy、strcat、strcmp
size_t my_strlen(const char *str) {
size_t len = 0;
while (*str) {
len++;
str++;
}
return len;
}
char *my_strcpy(char *dest, const char *src) {
char *temp = dest; // 暂存dest字符串的首地址
while (*src) {
*dest = *src;
dest++;
src++;
}
*dest = '\0'; // 在目标字符串末尾加上 null 终止符
return temp;
}
char *my_strcat(char *dest, const char *src) {
char *temp = dest;
// 移动 dest 指针到目标字符串的末尾
while (*dest) {
dest++;
}
// 将源字符串复制到目标字符串的末尾
while (*src) {
*dest = *src;
dest++;
src++;
}
// 添加目标字符串的 null 终止符
*dest = '\0';
return temp;
}
#include <stdio.h>
int my_strcmp(const char *str1, const char *str2) {
while (*str1 && *str2) {
if (*str1 != *str2) {
return *str1 - *str2;
}
str1++;
str2++;
}
// 如果两个字符串长度不等,返回长度差值
return *str1 - *str2;
}
int main() {
const char *str1 = "apple";
const char *str2 = "banana";
int result = my_strcmp(str1, str2);
if (result < 0) {
printf("%s 小于 %s\n", str1, str2);
} else if (result == 0) {
printf("%s 等于 %s\n", str1, str2);
} else {
printf("%s 大于 %s\n", str1, str2);
}
return 0;
}
5.打印杨辉三角前10行
#include <stdio.h>
int main(int argc, char const *argv[])
{
int a[10][10] = {0}, i, j;
for (i = 0; i < 10; i++)
{
a[i][0] = 1;
for (j = 1; j <= i; j++)
a[i][j] = a[i - 1][j] + a[i - 1][j - 1];
}
for (i = 0; i < 10; i++)
{
for (j = 0; j <= i; j++)
printf("%-5d", a[i][j]);
putchar('\n');
}
return 0;
}运行上述代码,将会输出杨辉三角的前10行:
Copy code
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1
1 8 28 56 70 56 28 8 1
1 9 36 84 126 126 84 36 9 1
每一行的数字都代表一个组合数,它是由 C(n, k) 组成,其中 n 为行数减1,k 为列数减1。在这个例子中,C(n, k) = triangle[n][k]。
6.atoi函数自定义
int my_atoi(const char *str) {
int result = 0;
int sign = 1;
// 跳过字符串前面的空格字符
while (*str == ' ') {
str++;
}
// 判断符号位
if (*str == '-' || *str == '+') {
sign = (*str == '-') ? -1 : 1;
str++;
}
// 将数字字符转换为整数,并累加到 result
while (*str >= '0' && *str <= '9') {
result = result * 10 + (*str - '0');
str++;
}
return result * sign;
}
- 定义一个函数计算一个字节里(byte)里面有多少bit被置1/
int count(int x) { int num=0; for (int i = 0; i < 8; i++)//一个字节是8位 { if((x&(1<<i))!=0) num++; } return num; } |
二、linuxC:
1.makefile包含哪几部分
# 编译器和编译选项
CC := gcc
CFLAGS := -Wall -Werror -g -c
# 目标文件和可执行文件名
OBJ_FILES := main.o utils.o
TARGET := my_program
# 默认规则:生成可执行文件
$(TARGET): $(OBJ_FILES)
$(CC) $(CFLAGS) $^ -o $@
# 生成目标文件 main.o
main.o: main.c utils.h
$(CC) $(CFLAGS) -c $< -o $@
# 生成目标文件 utils.o
utils.o: utils.c utils.h
$(CC) $(CFLAGS) -c $< -o $@
# 伪目标:清理临时文件 特殊规则(还可以安装文件)
.PHONY: clean
clean:
rm -f $(TARGET) $(OBJ_FILES)
# 注释以 # 开头,$@ 表示目标文件,$< 表示第一个依赖文件,$^ 表示所有依赖文件等。
2.linux命令
文件和目录操作:
ls: 列出文件和目录
cd: 切换目录
pwd: 显示当前工作目录
touch: 创建空文件或更新文件的访问时间
mkdir: 创建新目录
cp: 复制文件或目录
mv: 移动文件或目录
rm: 删除文件或目录
cat: 查看文件内容
more 或 less: 分页查看文件内容
head: 查看文件开头部分
tail: 查看文件结尾部分
文件内容处理:
grep: 在文件中搜索指定字符串
find: 在目录中查找文件
wc: 统计文件中的行数、字数和字节数
sort: 对文件内容进行排序
uniq: 删除文件中的重复行
cut: 从文件中截取字段
sed: 流编辑器,用于处理文本流
文件权限和所有权管理:
chmod: 修改文件或目录的权限
chown: 修改文件或目录的所有者和所属
系统信息查看:
date: 显示或设置系统时间和日期
uptime: 查看系统运行时间和负载
ps: 显示进程状态
top: 实时显示系统进程状态
df: 查看磁盘空间使用情况
du: 查看文件和目录的磁盘使用情况
网络命令:
ping: 测试网络连通性
ifconfig 或 ip: 显示或配置网络接口信息
netstat: 显示网络状态信息
ssh: 安全远程登录
scp: 安全复制文件或目录
wget 或 curl: 下载文件或网页
系统管理:
reboot: 重启系统
shutdown: 关闭系统
useradd: 添加新用户
passwd: 修改用户密码
userdel: 删除用户
su: 切换用户
压缩和解压缩:
tar: 打包和解包文件 -xvf
gzip 或 gunzip: 压缩和解压缩文件
zip 或 unzip: 压缩和解压缩zip文件
3.<>和""的区别
- <>:用于包含系统提供的标准库头文件或编译器所附带的头文件。编译器会在系统默认的头文件搜索路径中查找这些头文件。通常,这些头文件的路径信息已经配置在编译器中,无需指定具体路径。示例:#include <stdio.h>
- "":用于包含用户自定义的头文件或在当前工作目录下的头文件。编译器会首先在当前源文件所在的目录中查找头文件,如果没有找到,则在系统默认的头文件搜索路径中查找。示例:#include "my_header.h"
4.#ifndef的作用
#ifndef是C语言预处理指令中的条件有编译指令之一,其作用是防止头文件被多次包含,从而避免重复定义错误。
#ifndef HEADER_FILE_NAME_H
#define HEADER_FILE_NAME_H
// 头文件内容
#endif
5.gdb调试
启动GDB:在终端中运行gdb命令,然后在GDB提示符中输入要调试的可执行文件的路径,例如:gdb ./my_program。
设置断点: 使用break命令设置断点,例如:break main在main函数处设置断点。
运行程序:在GDB中使用run或r命令运行程序,例如:run。
单步执行:使用step或s命令单步执行程序,按行执行并进入函数。
运行到断点:使用continue或c命令运行程序,直到遇到下一个断点。
查看变量:使用print或p命令查看变量的值,例如:print my_variable。
查看堆栈:使用backtrace或bt命令查看函数调用堆栈。
跟踪变量:使用watch命令跟踪 变量的值变化,例如:watch my_variable。
删除断点:使用delete命令删除断点,例如:delete 1删除序号为1的断点。
退出GDB:使用quit或q命令退出GDB。
6.gcc编译步骤
编译:接下来,对预处理后的文件进行编译 。
gcc -S hello.i -o hello.s
汇编:将汇编代码转换成机器代码。
gcc -C hello.s -o hello.o
链接:最后,将目标文件和其他可能需要的库文件链接在一起,生成可执行文件。
gcc hello.o -o hello
三、数据结构:
1. 单链表的增删改查操作、实现思路
link_node_t *CreateEpLinkList()
{
link_node_t *h = (link_node_t *)malloc(sizeof(link_node_t));
/*开辟空间,返回值为结构体指针,开辟的空间强转为结构体指针,空间大小为结构体大小*/
if (NULL == h)
{
printf("开辟空间失败\n");
return NULL;
}
h->next = NULL;
return h;
}
//2.向单向链表的指定位置插入数据,p保存链表的头指针 post 插入的位置 data插入的数据
int InsertIntoPostLinkList(link_node_t *p, int post, datatype data)
{
/*创建新节点 */n
link_node_t *pnew = CreateEpLinkList();
pnew->next = NULL; /*链接节点*/
pnew->data = data; /*写入数据*/
/*进行插入 */
if (post < 0 || p == NULL)
{
printf("插入失败\n");
return -1;
}
for (int i = 0; i < post; i++)
{
p = p->next;
}
pnew->next = p->next; /*链接节点 尾插第一个p->next为空.
头插和中间插p->next为下一个数据*/
p->next = pnew;
return 0;
}
//5.删除单向链表中指定位置的数据 post 代表的是删除的位置
int DeletePostLinkList(link_node_t *p, int post)
{
if (post < 0 || post >= LengthLinkList(p) || IsEpLinkList(p))
{
printf("删除错误\n");
}
for (int i = 0; i < post; i++)
{
p = p->next;
}
link_node_t *p_del = p->next;
p->next = p_del->next;
free(p_del);
p_del = NULL
return 0;
}
//10.删除单向链表中出现的指定数据,data代表将单向链表中出现的所有data数据删除
int DeleteDataLinkList(link_node_t *p, datatype data)
{
if (IsEpLinkList(p)) /*判断链表是否为空*/
return -1;
link_node_t *p_del = NULL;
while (p->next != NULL) /*循环终止条件为p->指向空*/
{
if (p->next->data == data) 看i修辞学
{
p_del = p->next;
p->next = p_del->next;
free(p_del);
p_del = NULL;
}
}
return 0;
}
//7.修改指定位置的数据 post被修改的位置 data修改成的数据
int ChangePostLinkList(link_node_t *p, int post, datatype data)
{
if (post < 0 || post >= LengthLinkList(p) || IsEpLinkList(p))
{
printf("修改失败\n");
return -1;
}
for (int i = 0; i <= post; i++)
{
p = p->next;
}
p->data = data; /*链接节点 */
return 0;
}
//8.查找指定数据出现的位置 data被查找的数据 //search 查找
int SearchDataLinkList(link_node_t *p, datatype data)
{
if (IsEpLinkList(p)) /*判断链表是否为空*/
return -1;
int i = 0;
while (p->next != NULL) /*循环终止条件为p->指向空*/
{
p = p->next;
if (p->data == data)
{
return i;
}
i++;
}
}
2. 单链表的倒置思路
//9.转置链表
void ReverseLinkList(link_node_t *p)
{
link_node_t *q = p->next;
link_node_t *temp = NULL;
/*将头节点和下一个节点断开 */
p->next = NULL;
/*遍历无头单向节点 */
while (q != NULL)
{
temp = q->next;
/*进行插入操作 */
q->next = p->next;
p->next = q;
q = temp;
}
}
3. 单链表和双向链表的区别
单链表(Singly Linked List)和双向链表(Doubly Linked List)是两种常见的链式存储结构,它们在数据结构中有一些不同之处。
单链
- 单链表中的每个节点包含两个部分:数据域和指针域(通常称为next指针)。
- 每个节点只有一个指针,指向下一个节点,最后一个节点的next指针指向NULL,表示链表结束。
- 单链表只能从头节点开始依次遍历访问节点,无法从后往前访问。
- 在单链表中,如果要删除某个节点,需要知道其前驱节点,因为只有前驱节点的next指针可以修改。
- 双向链表中的每个节点包含三个部分:数据域、前驱指针(prev指针)和后继指针(next指针)。
- 每个节点有两个指针,分别指向前一个节点和后一个节点。第一个节点的prev指针和最后一个节点的next指针指向NULL,表示链表的开始和结束。
- 双向链表可以从头节点开始正向遍历访问节点,也可以从尾节点开始反向遍历访问节点,这使得双向链表的某些操作更加高效。
- 在双向链表中,如果要删除某个节点,可以直接通过前驱指针和后继指针完成删除操作,不需要只知道其前驱节点。
- 单链表每个节点只有一个指针域,只能从头节点开始单向遍历。
- 双向链表每个节点有两个指针域,可以从头节点开始正向遍历,也可以从尾节点开始反向遍历,灵活性更高。
- 双向链表相对于单链表,每个节点多了一个prev指针,这增加了一定的内存开销。
- 在插入和删除操作上,双向链表的操作可能比单链表更复杂一些,因为需要同时修改前驱和后继节点的指针。但是在一些情况下,双向链表的操作效率更高,特别是对于涉及到反向遍历或删除操作时。
4. 栈和队列的区别
- Last In First Out,LIFO)的数据结构,类似于把元素放在一个垂直的栈中,最后放入的元素最先被取出。
- First In First Out,FIFO)的数据结构,类似于排队,最先放入的元素最先被取出。
- Push),删除操作称为出栈(Pop)。元素只能从栈顶进。
- Enqueue),删除操作称为出队(Dequeue)。元素只能从队列的前端出队,从队列的后端入队。
5. 两个栈怎样实现一个队列,思路说一下
- 使用栈 A 作为入队栈,栈 B 作为出队栈。
- 入队操作时,直接将元素压入栈 A。
- 出队操作时,首先检查栈 B 是否为空,如果不为空‘’,则直接弹出栈顶元素;如果栈 B 为空,则将栈 A 中的所有元素逐个弹出并压入栈 B,然后再弹出栈 B 的栈顶元素作为出队元素。
- 冒泡排序原理?时间复杂度多少?
原理:冒泡排序是一种简单的排序算法,每次比较相邻的两个元素,如果它们的顺序错误,则交换位置,通过多次遍历,将最大的元素逐渐冒泡到正确的位置上。
时间复杂度:冒泡排序的平均时间复杂度为O(n²),其中n是待排序元素的数量,最好情况下时间复杂度为O(n)
- 什么是二叉树、满二叉树?
二叉树:是一种特殊的树结构,每个节点最多有两个子树,分别称为左子节点和右子节点
满二叉树:除了叶子节点外的每一个节点都有两个子节点。
- 二叉树的前序中序后序遍历
(1)前序遍历:先访问根节点,然后按照左子树、右子树的顺序进行遍历
(3)后序遍历:按照左子树、右子树、根节点的顺序进行遍历
9. 查找算法学过哪些 ?二分查找的时间复杂度多少?
二分查找:对有序数组进行查找,每次将查找区间二分,并与目标值进行比较,时间复杂度为O(nlog n )
10. 哈希表的原理?
哈希表:是一种通过哈希函数将键映射到存储位置的数据结构。查找元素时,先使用哈希函数计算出键的哈希值,然后根据哈希值找到对应的存储位置,在哈希冲突的情况下,通常采用开放地址法、活链地址法来解决.
冒泡排序:比较相邻元素,将较大的元素向后移动,时间复杂度为O(n²)
选择排序:从数列中找最小值,找到后和第一个位置的数据进行交互,再从剩下数中找最小值,依次类推。
快速排序:采用“分治”的思想,对于一组数据,选择一个基准元素(base),通常选择第一个或最后一个元素,通过第一轮扫描,比base小的元素都在base左边,比base大的元素都在base右边,再有同样方法递归排序这两部分,直到序列中所有数据均有序为止。