数据结构和算法-小甲鱼【笔记】
数据结构和算法-小甲鱼
序论
程序设计 = 数据结构 + 算法
数据结构就是关系--数据元素相互之间存在的一种或多种特定关系的集合
- 逻辑结构:数据对象中数据元素间的相互关系
- 物理结构:数据的逻辑结构在计算机中的存储形式
逻辑结构:
- 集合结构:集合结构中的数据元素除了同属于一个集合外,他们之间没有其他关系
- 线性结构:线性结构中的数据元素之间是一对一的关系
- 树形结构:树形结构中的数据元素之间存在一种一对多的层次关系
- 图形结构:图形结构的数据元素是多对多的关系
数据结构:把数据元素存储到计算机的存储器中
存储器主要是针对内存而言,像硬盘、软盘、光盘等外部存储器的数据组织通常用文件结构来描述。
-
顺序存储结构:把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的
-
链式存储结构:把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。通过一个指针存放数据元素的地址,可以通过地址找到相关的关联数据元素的位置。
算法
算法:解决特定问题求解步骤的描述,在计算机中表现位指令的有限序列,并且每条指令表示一个或多个操作。
五个基本特征:输入、输出、有穷、确定性、可行性
-
输入:算法具有零个或多个输入
-
输出:算法至少有一个或多个输出
-
有穷性:算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成
-
确定性:
- 算法的每一个步骤都具有确定的含义,不会出现二义性。
- 算法在一定条件下,只有一条执行路径,相同的输入只能有唯一的输出结果。
- 算法的每个步骤都应该被精确定义而无歧义。
-
可行性:算法的每一步都必须是可行的,即,每一步都能通过执行有限次数完成。
算法设计的要求:
- 正确性:
- 算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反应问题的需求、能够得到问题的正确答案。
- 大体分为四个层次:
- 算法程序没有语法错误。
- 算法程序对于合法输入能够产生满足要求的输出。
- 算法程序对于非法输入能够产生满足规格的说明。
- 算法程序对于故意***难的测试输入都有满足要求的输出结果。
- 可读性 :便于阅读、理解和交流
- 健壮性:当输入数据不合法时,算法也能做出相关处理,而不是产生异常、崩溃或莫名其妙的结果。
- 时间效率高和存储量低
算法效率的度量方法
度量一个算法的执行时间
- 事后统计方法:
- 通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。
- 缺陷:必须依据算法事先编制好测试程序,通常需要花费大量时间和精力
- 事前分析估算方法:在计算机程序编写前,依据统计方法对算法进行估算
高级语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
1. 算法采用的策略,方案 2. 编译产生的代码质量 3. 问题的输入规模 4. 机器执行指令的速度
抛开计算机硬件、软件有关因素,一个程序的运行时间依赖于算法的好坏和问题的输入规模。
在分析程序的运行时间时,最重要的是把程序看成是独立与程序设计语言的算法或一系列步骤。
在分析一个算法的运行时间时,重要的是把基本操作的数量和输入模式关联起来。
函数的渐进增长:
给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么,我们说f(n)的增长渐进快于g(n);
与最高项相乘的常数并不重要,可以忽略;
最高次项的指数大,函数随n的增长,结果也会变得的增长特别快。
判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,更应该关注主项(最高项)的阶数
算法时间复杂度
定义:在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况,并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n) = O(f(n))。他表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
大O记法:O()
一般情况下,随着输入规模n的增大,T(n)增长最慢的算法为最优算法。
分析一个算法的时间复杂度攻略:
-
用常数1取代运行时间中的所有加法常数。
-
在修改后的运行次数函数中,只保留最高阶项。
-
如果最高阶项存在且不是1,则去除与这个项相乘的常数。
-
得到的最后结果就是大O阶。
常数阶:O(1)
线性阶:O(n) 一般非嵌套循环涉及线性阶,线性阶就是随着问题规模n的扩大,对应计算次数呈直线增长。
平方阶:O(n^2) 循环的时间复杂度等于循环题的复杂度乘以该循环运行的次数。
立方阶:O(n^3)
对数阶:O(logn)
指数阶:O(2^n)
......
-
常见的时间复杂度:
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
平均运行时间:期望的运行时间
最坏运行时间:一种保证。在应用中,这是一种最重要的需求。通常除非特别指定,我们提到的运行时间都是最坏情况的运行时间。
算法空间复杂度:
通过计算算法所需的存储空间实现,算法的空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。
线性表
线性表(List):由零个或多个数据元素组成的有限序列。
强调:
- 首先,它是一个序列,也就是说元素之间是有个先来后到的。 - 若元素存在多个,则第一个元素无前驱,而最后一个元素无后继,其他元素都有且只有一个前驱和后继。 - 另外,线性表强调有限的。
如果用数学语言来进行定义,可如下:
若将线性表记为(a1,...ai-1,ai,ai+1,...an),则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。
所以,线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表。
抽象数据类型
数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。
在C语言中,按照取值的不同,数据类型可以分为两类:
- 原子类型:不可以再分解的基本类型。 - 结构类型:由若干类型组合而成,是可以再分解的。
抽象:是指抽取出事物具有的普遍性的本质。它要求抽出问题的特征而忽略非本质的细节,是对具体事物的一个概括。抽象是一种思考问题的方式,它隐藏了繁杂的细节。
对已有的数据类型进行抽象,就得到了抽象数据类型。
抽象数据类型(Abstract Data Type,ADT),是指一个数学模型及定义在该模型上的一组操作。
抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关。
“抽象”的意义在于数据类型的数学抽象特性。
而且,抽象数据类型不仅仅指那些已经定义并实现的数据类型,还可以是计算机编程者在设计软件程序是自己定义的数据类型。
//抽象数据类型的标准格式: ADT 抽象数据类型名 Data 数据元素之间逻辑关系的定义 Operation 操作 endADT
线性表的抽象数据类型定义:
ADT 线性表(List) Data 线性表的数据对象集合为{a1,a2,...an},每个元素的类型。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。 Operation InitList(*L):初始化操作,建立一个空的线性表L。 ListEmpty(*L):判断线性表是否为空表,若线性表为空,返回true,否则返回false。 ClearList(*L):将线性表清空。 GetElem(L,i,*e):将线性表L中的第i个位置元素值返回给e。 LocateElem(L,e):在线性表L中查找于给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则,返回0表示失败。 ListInsert(*L,i,e):在线性表L中第i个位置插入新元素e。 ListDelete(*L,i,*e):删除线性表L中第i个位置元素,并用e返回其值。 ListLength(L):返回线性表L的元素个数。 endADT
//求线性表AUB //union.c //La表示A集合,Lb表示B集合 void unionL(*La, Lb){ int La_len, Lb_len, i; ElemType e; La_len = ListLength(*La); Lb_len = ListLength(Lb); for(int i=1; i<= Lb_len; i++) { GetIElem(Lb, i, &e); if(!LocateElem(*La, e)) { ListInsert(La, ++La_len, e); } } }
线性表的顺序存储结构
指的是用一段地址连续的存储单元依次存储线性表的数据元素。
物理上的存储方式事实上就是在内存中找个初始地址,然后通过占位的形式,把一定的内存空间给占了,然后把相同数据类型的数据元素依次放在这块空地中。
//线性表顺序存储的结构代码: #define MAXSIZE 20 typedef int ElemType; typedef struct { ElemType data[MAXSIZE]; int length; //线性表当前长度 }SqList;
总结,顺序存储结构封装需要三个属性:
- 存储空间的其实位置,数组`data`,它的存储位置就是线性表存储空间的存储位置。 - 线性表的最大存储容量:数组的长度`MaxSize`。 - 线性表的当前长度:`length`。
注意:数组的长度与线性表的当前长度需要区分。
- 数组的长度是存放线性表的存储空间的总长度,一般初始化后不变。 - 而线性表的当前长度是线性表中元素的个数,是会变化的。
假设ElemType占用的是c个存储单元(字节),那么线性表中第i+1个数据元素和第i个数据元素的存储位置的关系是(LOC表示获得存储位置的函数):LOC(ai+1) = LOC(ai) + c
所以对于第i个数据元素ai的存储位置可以由a1推算得出:LOC(ai) = LOC(a1) + (i-1)*c
存储时间性能:O(1)
,通常称为随机存储结构。
//getElem.c #define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 typedef int Status; //Status 是函数的类型,其值是函数结果抓过你太代码,如OK等。 //初始条件:顺序线性表L已存在, 1 <= i <= ListLength(L) //操作结果:用e返回L中第i个数据元素的值。 Status GetElem(SqList L, int i, ElemType *e) { if(L.length==0 || i<1 || i>L.length) { return ERROR; } *e = L.data[i-1]; return OK; }
插入操作
插入算法的思路:
- 如果插入位置不合理,抛出异常; - 如果线性表长度大于等于数组长度,则抛出异常或动态增加数组容量; - 从最后一个元素开始向前遍历到第i个位置,分别将他们都向后移动一个位置; - 线性表长度加1;
Status ListInsert(SqList *L, int i, ElemType e) { int k; if(L->length == MAXSIZE) //顺序线性表已经满了 { return ERROR; } if(i<1 || i>L->length+1) //当i不在范围内时 { return ERROR; } if(i <= L->length ) //若插入数据位置不在表尾 { for(k=L->length-1; k>=i; k--) { L->data[k+1] = L->data[k]; } } L->data[i-1] = e; //将新元素插入 L->length++; return OK; }
删除操作
- 如果删除位置不合理,抛出异常; - 取出删除元素; - 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一格位置; - 表长-1;
Status ListDelete(SqList *L, int i, ElemType *e) { int k; if(L->length == 0) { return ERROR; } if( i<1 || i> L->length) { return ERROR; } *e = L->data[i-1]; if( i < L->length) { for( k=i; k < L->length; k++) { L->data[k-1] = L->data[k]; } } L->length--; return OK; }
插入和删除的时间复杂度:最好O(1)、最坏O(n)、平均O(n)
线性表顺序存储结构的优缺点:
- 在存储、读取数据时,时间复杂度O(1); 在插入、删除时,时间复杂度O(n); - 适合元素个数比较稳定,不经常插入和删除元素,而更多的操作时存取数据的应用;
- 优点:
- 无须为表示表中元素之间的逻辑关系而增加额外的存储空间;
- 可以快速地存取表中任意位置的元素
- 无须为表示表中元素之间的逻辑关系而增加额外的存储空间;
- 缺点:
- 插入和删除操作需要移动大量元素。
- 当线性表长度变化比较大时,难以确定存储空间的容量。
- 容易造成存储空间的"碎片"。
线性表链式存储结构
特点:用一组任意的存储单元存储线性表的数据元素,这组存储单元可以存在内存中未被占用的任意位置。
链式存储结构中,除了存储数据元素信息外,还存储它的后继元素的存储地址(指针)。即,存储本身信息外,还存储一个指示其直接后继的存储位置的信息。
结点(Node),存储映像:
- 数据域:存储数据元素信息的域 - 指针域:存储直接后继位置的域。指针域中存储的信息称为指针或链。
n个结点链接成一个链表,即为线性表(a1, a2, a3, ..., an)的链式存储结构。
单链表:链表的每个结点中只包含一个指针域。
链表中的第一个结点的存储位置叫做头指针,最后一个结点指针为空(NULL)
头指针与头节点的异同:
- 头指针
- 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
- 头指针具有标识作用, 所以常用头指针冠以链表的名字(指针变量的名字)
- 无论链表是否为空,头指针均不为空。
- 头指针是链表的必要元素。
- 头结点
- 头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以来存放链表的长度)。
- 有了头结点,对在第一元素结点前插入结点和删除第一结点起操作与其它结点的操作就统一了。
- 头结点不一定是链表的必须要素。
//结构指针:描述单链表 typedef struct Node { ElemType data; //数据域 struct Node* Next; //指针域 }Node; typedef struct Node* LinkList;
假设p是指向线性表第i个元素的指针,则该结点ai的数据域为p->data的值,结点ai的指针域为p->next.
单链表的读取:
思路:
- 声明一个结点p指向链表的第一个结点,初始化j从1开始; - 当j<i时,遍历链表,让p的指针向后移动,不断指向下一个结点,j+1; - 若到链表末尾,p为空,则说明第i个元素不存在; - 否则查找成功,返回结点p的数据。
//时间复杂度: O(n) //核心思想:“工作指针后移” Status GetElem(SqList L, int i, ElemType *e) { int j; LinkList p; p = L->next; j = 1; while( p && j<i ) { p = p->next; ++j; } if( !p || j >i) { return ERROR; } *e = p->data; return OK; }
单链表的插入
假设存储元素e的结点为s,要实现结点p、p->next和s之间逻辑关系
让s->next和p->next的指针改变:
s->next = p->next;
p->next = s;
单链表第i个数据插入结点的算法思路:
- 声明一结点p指向链表头结点,初始化j从1开始; - 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1; - 若到链表末尾p为空,则说明第i个元素不存在; - 否则查找成功,再系统中生成一个空结点s; - 将数据元素e赋值给s->data; - 单链表的插入刚才两个标准语句;`s->next = p->next; p->next = s;` - 返回成功。
//时间复杂度:O(n) Status ListInsert(LinkList *L, int i, ElemType e) { int j; LinkList p,s; p = *L; j = 1; while(p && j<i) //用于寻找第i个结点 { p = p->next; j++; } if( !p || j>i) { return ERROR; } s = (LinkList)malloc(sizeof(Node)); s->data = e; s->next = p->next; p->next = s; return OK; }
单链表的删除
假设元素a2的结点为q,要实现结点q删除单链表的操作,其实就是将它的前继结点的指针绕过指向后继结点即可。
p->next = p->next->next;
或 q=p->next; p->next = q->next;
单链表第i个数据删除结点的算法思路:
- 声明结点p指向链表第一个结点,初始化j=1; - 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1; - 若到链表末尾p为空,则说明第i个元素不存在; - 否则查找成功,将欲删除结点p->next赋值给q; - 单链表的删除标准语句p->next = q->next; - 将q结点中的数据赋值给e,作为返回; - 释放q结点。
//时间复杂度:O(n) Status ListDelete(LinkList *L, int i, ElemType *e) { int j; LinkList p,q; while( p && j<i) { p = p->next; ++j; } if( !(p->next) || j>i) { return ERROR; } q = p->next; p->next = q->next; *e = q->data; free(q); return OK; }
在找到第i个元素后,后续的插入、删除的时间复杂度是O(1);
即,插入或删除数据频繁时,单链表效率优势明显。
单链表的整表创建
数据分散、增长动态
对于每个链表来说,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。
静态链表
//线性表的静态链表存储结构 #define MAXSIZE 1000 typedef struct { ElemType data; //数据 int cur; //游标(Cursor) }Component, StaticLinkList[MAXSIZE];
//初始化静态链表 Status InitList(StaticLinkList space) { int i; for( i=0; i < MAXSIZE-1; i++) space[i].cur = i + 1; space[MAXSIZE - 1].cur = 0; return OK; }
注意:
- 对数组的第一个和最后一个元素做特殊处理,其data不存放数据。
- 通常把未使用的数组元素称为备用链表。
- 数组的第一个元素,即下标为0的那个元素的cur存放备用链表的第一个结点下标。
- 数组的最后一个元素,即下标为MAXSIZE-1的cur存放第一个有数值的下标,相当于单链表中的头结点作用。
- 数组中已使用最后一个元素的cur为0??
静态链表的插入操作
操作的是数组,需要自己实现结点的申请和释放。
辨明数组中哪些分量未被使用的方法:将所有未被使用过的及已被删除的用游标链成一个备用链表
//获得空闲分量下标 int Malloc_SLL(StaticLinkList space) { int i = space[0].cur; if(space[0].cur) space[0].cur = space[i].cur; //把它当前指向的下一个分量作为备用链表的头结点 return i; }
//在静态链表L中第i个元素之前插入新的数据元素e Status ListInsert(StaticLinkList L, int i, ElemType e) { int j, k, l; k = MAXSIZE -1; //取出当前指向的第一个元素的下标 if( i<1 || i>ListLength(L)+1) { return ERROR; } j = Malloc_SLL(L); //分配结点资源 if(j) { //结点分配成功 L[i].data = e; //元素存入新申请的结点 for( l=1; l<= i-1; l++) { k = l[k].cur; } L[j].cur = L[k].cur; //设置新申请结点的游标指向原有的游标 L[k].cur = j; //设置原有的的游标为新申请的结点 return OK; } return ERROR; }
//删除在L中的第i个数据元素 Status ListDelet(StaticLinkList L, int i) { int j, k; if( i<1 || i>ListLength(L)) { return ERROR; } k = MAXSIZE - 1; for( j=1; j<= i-1; j++) { k = L[K].cur; } j = L[k].cur; L[k].cur = L[j].cur; Free_SLL(L, j); return OK; } //将下标为k的空闲结点回收到备用链表 void Free_SLL(StaticLinkList space, int k) { space[k].cur = space[0].cur; space[0].cur = k; }
//返回L中数据元素个数 int ListLength(StaticLinkList L) { int j = 0; int i = L[MAXSIZE-1].cur; while(i) { i = L[i].cur; j++; } return j; }
静态链表优缺点总结:
- 优点
- 在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点。
- 缺点
- 没有解决连续存储分配(数组)带来的表长难以确定的问题。
- 失去了顺序存储结构随机存取的特性
即,静态链表是为了给没有指针的编程语言设计的。
例题:快速找到未知长度单链表的中间结点--快慢指针
例题:快速找到未知长度单链表的中间结点 - 普通方法: 首先,遍历一遍单链表,以便确定单链表的长度L。 然后,再次从头结点触发循环L/2次找到单链表的中间结点 算法复杂度为O(L+L/2)=O(3L/2) - 快慢指针: 原理:设置两个指针*search、*mid都指向单链表的头结点。其中*search的移动速度是*mid的2倍。当*search指向末尾节点的时候,mid正好指向中间结点。 即标尺的思想。
Status GetMidNode(LinkList L, ElemType *e) { LinkList search,mid; mid = search = L; while(search->next != NUll) { if(search->next->next != NULL) { search = search->next->next; mid = mid->next; } else { search = search->next; } } *e = mid->data; return OK; }
循环链表
将单链表中终端结点的指针端由空指针改为指向头结点,使得整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。 判断空链表的条件:head->next是否等于head 终端结点用尾指针rear指示,则查找终端结点是O(1),开始结点rear->nexe->next,也是O(1).
//链表存储结构定义 typedef struct CLinkList { int data; struct ClinkList *next; }node;
//初始化循环链表 void ds_init(node **pNode) { int item; node *temp; node *target; printf("请输入结点的值,输入0完成初始化\n"); while(1) { scanf("%d", &item); fflush(stdin); if(item == 0) return; if((*pNode) == NULL) { //循环链表中只有一个结点 *pNode = (node*)malloc(sizeof(strut CLinkList)); if(!(*pNode)) exit(0); (*pNode)->data = item; (*pNode)->next = *pNode; } else { //找到next指向第一个结点的结点 for(target = (*pNode); target->next != (*pNode); target = target->next) ; //生成一个新的结点 temp = (node*)malloc(sizeof(struct ClinkList)); if(!temp) exit(0); temp->data = item; temp->next = *pNode; target->next = temp; } } }
//插入结点 void ds_insert(node **pNode, int i) { node *temp; node *target; node *p; int item; int j = 1; printf("输入要插入结点的值:"); scanf("%d", &item); if(i == 1) { //新插入的结点作为第一个结点 temp = (node*)malloc(sizeof(struct CLinkList)); if(!temp) exit(0); temp->data = item; //寻找到最后一个结点 for(target = (*pNode); target->next != (pNode); target = target->next) ; temp->next = (*pNode); target->next = temp; (*pNode) = temp; } else { target = *pNode; for(; j<i-1;++j) { target = target->next; } temp = (node*)malloc(sizeof(struct CLinkList)); if(!temp) exit(0); temp->data = item; p = target->next; temp->next = p; target->next = temp; } } //删除结点 void ds_delete(node **pNode, int i) { node *target; node *temp; int j = 1; if(i == 1) //删除第一个结点 { //找到最后一个结点 for(target = *pNode; target->next != *pNode; target = target->next) ; temp = *pNode; *pNode = *pNode->next; target->next = *pNode; free(temp); } else { target = *pNode; for(; j<i-1; ++j) { target = target->next; } temp = target->next; target->next = temp->next; free(temp); } } //返回结点所在位置 int ds_search(node **pNode, int elem) { node *target; int i = 1; for(target = *pNode; target->data != elem && target->next != *pNode; ++i) target = target->next; if(target->next == *pNode) //链表中不存在该元素 return 0; else return i; }
约瑟夫问题
描述:
据说著名犹太历史学家Josephus有过以下的故事: 在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要把被敌人抓到,于是决定了一个自杀方式,41个人拍成一个圆圈,由第一个人开始报数,每报数到第3个人,该人就必须自杀,然后再有下一个重新报数,直到所有人都自杀身亡为止。 然而Josephus和他的朋哟并不想遵从,他们假意遵从,却将自己排在第16与第31个位置,从而逃过死亡游戏。
//n人围圈报数 #include <stdio.h> #include <stdlib.h> typedef struct node { int data; struct node *next; }node; node *create(int n) { node *p = NULL, *head; head = (node*)malloc(sizeof(node)); //头结点 p = head; node *s; int i = 1; if(0 != n) { while( i <= n) { s = (node *)malloc(sizeof(node)); s->data = i++; p->next = s; p = s; } s->next = head->next; } free(head); return s->next; } int main() { int n = 41; int m = 3; int i; node *p = create(n); node *temp; m %= n; while(p != p->next) { for( i=1 ; i < m-1; i++) { p = p->next; } printf("%d->", p->next->data); temp = p->next; p->next = temp->next; free(temp); p = p->next; /*根据理解自己修改后如下: node *before; for( i=1 ; i < m; i++) { before = p; p = p->next; } printf("%d->", p->data); before->next = p->next; temp = p->next; free(p); p = temp; */ } printf("%d\n", p->data); return 0; }
循环链表的特点
用O(1)的时间就可以由链表指针访问到最后一个结点。
用指向终端结点额尾指针来表示循环链表
判断是否为空链表的条件为:判断rear是否等于rear->next
循环链表的特点:无须增加存储量,进队链接方式稍作改变即可使处理更加方便灵活。
一道例题
题目:实现将两个线性表(a1,a2,...,an)和(b1,b2,...,bm)连接成一个线性表(a1,...,an,b1,...,bm)的运算。
分析:
- 链表或头指针表示的单循环链表,需要遍历第一个链表,O(n) - 尾指针表示的单循环链表,只需要修改指针,无须遍历,O(1)
//假设A,B为非空循环链表的尾指针 LinkList Connect(LinkList A,LinkList B) { LinkList p = A->next; //保存A表的头结点位置 A->next = B->next->next; //B表的开始结点链接到A表尾 free(B->next); //释放B表的头结点,初学者容易忘记 B->next = p; return B; //返回新循环链表的尾指针 }
判断单链表中是否有环
有环:链表的尾结点指向了链表中的某个结点。
方法两种:
- 方法1:使用p、q两个指针,p总是向前走,但q每次都从头开始走,对于每个节点,看p走的步数是否和q一样。若不相等则存在环。 - 方法2:快慢指针。使用p、q两个指针,p每次向前走一步,q每次向前走两步,若在莫格时候p==q,则存在环。
#include "stdio.h" #define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */ typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */ typedef struct Node { ElemType data; struct Node *next; }Node, *LinkList; /* 初始化带头结点的空链表 */ Status InitList(LinkList *L) { *L = (LinkList)malloc(sizeof(Node)); /* 产生头结点,并使L指向此头结点 */ if(!(*L)) /* 存储分配失败 */ return ERROR; (*L)->next=NULL; /* 指针域为空 */ return OK; } /* 初始条件:顺序线性表L已存在。操作结果:返回L中数据元素个数 */ int ListLength(LinkList L) { int i=0; LinkList p=L->next; /* p指向第一个结点 */ while(p) { i++; p=p->next; } return i; } /* 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */ void CreateListHead(LinkList *L, int n) { LinkList p; int i; srand(time(0)); /* 初始化随机数种子 */ *L = (LinkList)malloc(sizeof(Node)); (*L)->next = NULL; /* 建立一个带头结点的单链表 */ for (i=0; i < n; i++) { p = (LinkList)malloc(sizeof(Node)); /* 生成新结点 */ p->data = rand()%100+1; /* 随机生成100以内的数字 */ p->next = (*L)->next; (*L)->next = p; /* 插入到表头 */ } } /* 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */ void CreateListTail(LinkList *L, int n) { LinkList p,r; int i; srand(time(0)); /* 初始化随机数种子 */ *L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */ r = *L; /* r为指向尾部的结点 */ for (i=0; i < n; i++) { p = (Node *)malloc(sizeof(Node)); /* 生成新结点 */ p->data = rand()%100+1; /* 随机生成100以内的数字 */ r->next=p; /* 将表尾终端结点的指针指向新结点 */ r = p; /* 将当前的新结点定义为表尾终端结点 */ } r->next = (*L)->next->next; } // 比较步数的方法 int HasLoop1(LinkList L) { LinkList cur1 = L; // 定义结点 cur1 int pos1 = 0; // cur1 的步数 while(cur1) { // cur1 结点存在 LinkList cur2 = L; // 定义结点 cur2 int pos2 = 0; // cur2 的步数 while(cur2) { // cur2 结点不为空 if(cur2 == cur1) { // 当cur1与cur2到达相同结点时 if(pos1 == pos2) // 走过的步数一样 break; // 说明没有环 else // 否则 { printf("环的位置在第%d个结点处。\n\n", pos2); return 1; // 有环并返回1 } } cur2 = cur2->next; // 如果没发现环,继续下一个结点 pos2++; // cur2 步数自增 } cur1 = cur1->next; // cur1继续向后一个结点 pos1++; // cur1 步数自增 } return 0; } // 利用快慢指针的方法 int HasLoop2(LinkList L) { int step1 = 1; int step2 = 2; LinkList p = L; LinkList q = L; while (p != NULL && q != NULL && q->next != NULL) { p = p->next; if (q->next != NULL) q = q->next->next; printf("p:%d, q:%d \n", p->data, q->data); if (p == q) return 1; } return 0; } int main() { LinkList L; Status i; char opp; ElemType e; int find; int tmp; i = InitList(&L); printf("初始化L后:ListLength(L)=%d\n",ListLength(L)); printf("\n1.创建有环链表(尾插法) \n2.创建无环链表(头插法) \n3.判断链表是否有环 \n0.退出 \n\n请选择你的操作:\n"); while(opp != '0') { scanf("%c",&opp); switch(opp) { case '1': CreateListTail(&L, 10); printf("成功创建有环L(尾插法)\n"); printf("\n"); break; case '2': CreateListHead(&L, 10); printf("成功创建无环L(头插法)\n"); printf("\n"); break; case '3': printf("方法一: \n\n"); if( HasLoop1(L) ) { printf("结论:链表有环\n\n\n"); } else { printf("结论:链表无环\n\n\n"); } printf("方法二:\n\n"); if( HasLoop2(L) ) { printf("结论:链表有环\n\n\n"); } else { printf("结论:链表无环\n\n\n"); } printf("\n"); break; case '0': exit(0); } } }
魔术师发牌问题
描述:利用一副牌中的13章黑牌,预先排好。第一张是A,再数两张是2依次翻出13张。
#include <stdio.h> #include <stdlib.h> #define CardNumber 13 typedef struct node { int data; struct node *next; }sqlist, *linklist; linklist CreateLinkList() { linklist head = NULL; linklist s, r; int i; r = head; for(i=1; i <= CardNumber; i++) { s = (linklist)malloc(sizeof(sqlist)); s->data = 0; if(head == NULL) head = s; else r->next = s; r = s; } r->next = head; return head; } // 发牌顺序计算 void Magician(linklist head) { linklist p; int j; int Countnumber = 2; p = head; p->data = 1; //第一张牌放1 while(1) { for(j=0; j < Countnumber; j++) { p = p->next; if(p->data != 0) //该位置有牌的话,则下一个位置 { p->next; j--; } } if(p->data == 0) { p->data = Countnumber; Countnumber ++; if(Countnumber == 14) break; } } } // 销毁工作 void DestoryList(linklist* list) j } int main() { linklist p; int i; p = CreateLinkList(); Magician(p); printf("按如下顺序排列:\n"); for (i=0; i < CardNumber; i++) { printf("黑桃%d ", p->data); p = p->next; } DestoryList(&p); return 0; }
双向链表
typedef struct DualNode { ElemType data; struct DualMode *prior; //前驱结点 struct DualMode *next; //后继结点 }DualNode,*DuLinkList;
//插入操作,插入s到p之前 //代码实现: s->next = p; s->prior = p->prior; p->prior->next = s; p->prior = s;
//删除操作,删除p结点 p->prior->next = p->next; p->next->prior = p->prior; free(p);
双向循环链表实践
要求:实现用户输入一个数使得26个子母的排列发生变化,出现移位操作。
栈
官方定义: 栈(Stack)是一个后进先出(Last in first out,LIFO)的线性表,要求只在表尾进行删除和插入操作。
解析:一个特殊的线性表(顺序表、链表),操作上有特殊要求和限制
- 栈的元素必须后进先出 - 栈的操作只能在这个线性表的表尾进行 - 注:对于栈来说,表尾称为栈的栈顶(top),相应的表头称为栈底(bottom)
栈的插入和删除操作
- 栈的插入操作(Push),叫做进栈,也称为压栈、入栈
- 栈的删除操作(Pop),叫做出栈,也称为弹栈
栈分为以下两种存储:
-
栈的顺序存储结构
-
栈的链式存储结构
最开始栈种不含有任何数据 ,叫做空栈,此时栈顶就是栈底。
然后数据从栈顶进入,栈顶栈底分离,整个栈的当前容量变大。
数据出栈时从栈顶弹出,栈顶下移,整个栈的当前容量变小。
栈的顺序存储结构
- 顺序存储的栈
//顺序存储的栈 typedef struct { ElemType *base; ElemType *top; int stackSize; }sqStack; /* 三个元素:base,top,stackSize. base:指向栈底的指针变量 top:指向栈顶的指针变量 stackSize:指示栈的当前可使用的最大容量 */
- 创建一个栈
//创建一个栈 #define STACK_INIT_SIZE 100 initStack(sqStack *s) { s->base = (ElemType *)malloc( STACK_INIT_SIZE * sizeof(ElemType)); if(!s->base) exit(0); s->top = s->base; //最开始,栈顶就是栈底 s->stackSize = STACK_INIT_SIZE; }
- 入栈操作
入栈操作又叫压栈操作,就是向栈中存放数据。
入栈操作要在栈顶进行,每次向栈中压入一个数据,top指针就要+1,直到栈满为止。
#define SATCKINCREMENT 10 Push(sqStack *s, ElemType e) { // 如果栈满,追加空间 if( s->top – s->base >= s->stackSize ) { s->base = (ElemType *)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType)); if( !s->base ) exit(0); s->top = s->base + s->stackSize; // 设置栈顶 s->stackSize = s->stackSize + STACKINCREMENT; // 设置栈的最大容量 } *(s->top) = e; s->top++; }
- 出栈操作
出栈操作就是在栈顶取出数据,栈顶指针随之下移的操作.
每当从栈内弹出一个数据,站的当前容量就-1.
Pop(SqStack *s, ElemType *e) { if( s->top == s->base) return; *e = *--(s->top); }
- 清空栈
ClearStack(sqStack *s){ s->top = s->base; }
- 销毁栈
DestroyStack(sqStack *s){ int i,len; len = s->stackSize; for( i=0; i<len; i++){ free(s->base); s->base++; } s->base = s->top = NULL; s->stackSize = 0; }
- 获取栈的长度
栈顶指针与栈底指针之差即为栈的长度。
int StackLen(saStack s) { return(s.top - s.base); }
栈的最大容量是指该栈占据内存空间的大小,其值是s.stackSize
- 利用栈的数据结构特点:二进制转8进制或16进制
-
二进制转八进制:3位二进制转1位八进制
-
二进制转十六进制:4位二进制转1位十六进制
栈的链式存储结构
栈链
typedef struct StackNode { ElemType data; //存放栈的数据 struct SatckNode *next; }StackNode, *LinkStackPtr; typedef struct LinkStack { LinkStackPtr top; //top指针 int count; //栈元素计数器 } Status Push(LinkStack *s, ElemType e) { LinkStackPtr p = (LinkStackPtr) malloc(sizeof(StackNode)); p->data = e; p->next = s->top; s->top = p; s->count++; return OK; } Staus Pop(LinkStack *s, ElemType *e) { LinkStackPtr p; if(StackEmpty(*S)) return ERROR; *e = s->top->data; p = s->top; s->top = s->top->next; free(p); s->count--; return OK; }
逆波兰表达式
- 后缀表达式
#include <stdio.h> #include <ctype.h> #include <stdlib.h> #define STACK_INIT_SIZE 20 #define STACKINCREMENT 10 #define MAXBUFFER 10 typedef double ElemType; typedef struct { ElemType *base; ElemType *top; int stackSize; }sqStack; InitStack(sqStack *s) { s->base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType)); if( !s->base ) exit(0); s->top = s->base; s->stackSize = STACK_INIT_SIZE; } Push(sqStack *s, ElemType e) { // 栈满,追加空间,鱼油必须懂! if( s->top - s->base >= s->stackSize ) { s->base = (ElemType *)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType)); if( !s->base ) exit(0); s->top = s->base + s->stackSize; s->stackSize = s->stackSize + STACKINCREMENT; } *(s->top) = e; // 存放数据 s->top++; } Pop(sqStack *s, ElemType *e) { if( s->top == s->base ) return; *e = *--(s->top); // 将栈顶元素弹出并修改栈顶指针 } int StackLen(sqStack s) { return (s.top - s.base); } int main() { sqStack s; char c; double d, e; char str[MAXBUFFER]; int i = 0; InitStack( &s ); printf("请按逆波兰表达式输入待计算数据,数据与运算符之间用空格隔开,以#作为结束标志: \n"); scanf("%c", &c); while( c != '#' ) { while( isdigit(c) || c=='.' ) // 用于过滤数字 { str[i++] = c; str[i] = '\0'; if( i >= 10 ) { printf("出错:输入的单个数据过大!\n"); return -1; } scanf("%c", &c); if( c == ' ' ) { d = atof(str); Push(&s, d); i = 0; break; } } switch( c ) { case '+': Pop(&s, &e); Pop(&s, &d); Push(&s, d+e); break; case '-': Pop(&s, &e); Pop(&s, &d); Push(&s, d-e); break; case '*': Pop(&s, &e); Pop(&s, &d); Push(&s, d*e); break; case '/': Pop(&s, &e); Pop(&s, &d); if( e != 0 ) { Push(&s, d/e); } else { printf("\n出错:除数为零!\n"); return -1; } break; } scanf("%c", &c); } Pop(&s, &d); printf("\n最终的计算结果为:%f\n", d); return 0; }
- 中缀表达式生成
#include <stdio.h> #include <stdlib.h> #define STACK_INIT_SIZE 20 #define STACKINCREMENT 10 typedef char ElemType; typedef struct { ElemType *base; ElemType *top; int stackSize; }sqStack; InitStack(sqStack *s) { s->base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType)); if( !s->base ) exit(0); s->top = s->base; s->stackSize = STACK_INIT_SIZE; } Push(sqStack *s, ElemType e) { // 栈满,追加空间,鱼油必须懂! if( s->top - s->base >= s->stackSize ) { s->base = (ElemType *)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType)); if( !s->base ) exit(0); s->top = s->base + s->stackSize; s->stackSize = s->stackSize + STACKINCREMENT; } *(s->top) = e; // 存放数据 s->top++; } Pop(sqStack *s, ElemType *e) { if( s->top == s->base ) return; *e = *--(s->top); // 将栈顶元素弹出并修改栈顶指针 } int StackLen(sqStack s) { return (s.top - s.base); } int main() { sqStack s; char c, e; InitStack( &s ); printf("请输入中缀表达式,以#作为结束标志:"); scanf("%c", &c); while( c != '#' ) { while( c>='0' && c<='9' ) { printf("%c", c); scanf("%c", &c); if( c<'0' || c>'9' ) { printf(" "); } } if( ')' == c ) { Pop(&s, &e); while( '(' != e ) { printf("%c ", e); Pop(&s, &e); } } else if( '+'==c || '-'==c ) { if( !StackLen(s) ) { Push(&s, c); } else { do { Pop(&s, &e); if( '(' == e ) { Push(&s, e); } else { printf("%c ", e); } }while( StackLen(s) && '('!=e ); Push(&s, c); } } else if( '*'==c || '/'==c || '('==c ) { Push(&s, c); } else if( '#'== c ) { break; } else { printf("\n出错:输入格式错误!\n"); return -1; } scanf("%c", &c); } while( StackLen(s) ) { Pop(&s, &e); printf("%c ", e); } return 0; }
队列
队列(queue)是只允许在有单进行插入操作,而在另一端进行删除操作的线性表
先进先出(First In First Out, FIFO)
- 顺序表
- 链表
链队列
typedef struct QNode{ ElemType *data; struct QNode *next; }QNode, *QueuePtr; typedef struct{ QueuePtr front,reat; //队头、队尾指针 }LinkQueue;
-
队头指针指向链队列的头结点
-
队尾指针指向终端结点
-
空队列时,front和reat都指向头结点
- 创建一个队列
- 内存中创建一个结点
- 队列的头指针和尾指针都指向生成的头结点【空队列】
//创建一个队列 initQueue(LinkQueue *q) { q->front = q->rear = (QueuePtr)malloc(sizeof(QNode)); if( !q->front) exit(0); q->front->next = NULL; }
- 入队列操作
InsertQueue(LinkQueue *q, ElemType e) { QueuePtr p; p = (QueuePtr)malloc(sizeof(QNode)); if(p == NULL) exit(0); p->data = e; p->next = NULL; q->rear->next = p; q->rear = p; }
- 出队列操作
DeleteQueue(LinkQueue *q, ElemType *e) { QueuePtr p; if(q->front == q->rear) return; p = q->front->next; *e = p->data; q->front->next = p->next; if(q->rear == p) q->rear = q->front; free(p); }
- 销毁队列
DestroyQueue(LinkQueue *q) { while( q->front ) { q->rear = q->front->next; free(q->front); q->front = q->rear; } }
队列的顺序存储结构
O(n)
循环队列
容量固定
队头与队尾指针
- 定义一个循环队列
#define MAXSIZE 100 typedef struct { ElemType *base; //用于存放内存分配基地址 ///可用数组存放 int front; int rear; }
- 初始化一个循环队列
initQueue(cycleQueue *q) { q->base = (ElemType*)malloc(MAXSIZE*sizeof(ElemType)); if( !q->base) exit(0); q->front = q->next = 0; }
- 入队列操作
InsertQueue(cycleQueue *q, ElemType e) { if((q->rear+1)%MAXSIZE == q->front) return; //队列已满 q->base[q->rear] = e; q->rear = (q->rear + 1) % MAXSIZE; }
- 出队列操作
DeleteQueue(cycleQueue *q, ElemType *e) { if(q->front == q->rear) return;//队列为空 *e = q->base[q->front]; q->front = (q->front+1)%MAXSIZE; }
递归与分冶
斐波那契数列Fibonacci 递归实现
F(n) = 0 // n=0 1 // n=1 F(n-1) +F(n-2) //n>1
- 迭代
//迭代 int main() { int i; int a[40]; a[0] = 0; a[1] = 1; printf("%d %d ", a[0], a[1]); for( i=2; i<40; i++) { a[i] = a[i-1] + a[i-2]; printf("%d ", a[i]); } }
- 递归
//递归 int Fib(int i) { if(i < 2) return i ==0 ?0:1; return Fib(i-1) + Fib(i-2); }
递归定义
递归函数:把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数
切记:每个递归定义必须至少有一个条件,当满足这个条件时递归不再进行,即函数不再调用自身而是返回。【结束条件】
递归与迭代的区别:
-
迭代使用的是循环结构
-
递归使用的是选择结构
使用递归能使程序的结构更清晰、更简洁、更容易让人理解,从而减少读懂代码的时间
但大量的递归调用会建立函数的副本,会消耗大量的时间和内存,而迭代则不需要此种付出。
递归函数分为调用和回退阶段,递归的回退顺序是它调用顺序的逆序。
//举例说明,计算n的阶乘n! int factorial(n) { if(0 == n) return 1; else return n*factorial(n-1); }
编写一个递归函数,实现将输入的任意长度的字符串反向输出的功能。 解: //以‘#’作为输入结束条件 void print() { char a; scanf("%c", &a); if( a != '#') print(); if( a != '#') printf("%c", a); }
分冶思想
折半查找算法的递归实现
汉诺塔
描述:三根柱子,64个大小不一的圆盘,保证小圆盘在大圆盘上,完成从一个柱子移动到另一个柱子上。
递归
#include <stdio.h> //将n个盘资从x借助y移动到z void move(int n, char y, char z) { if(1 == n) { printf("%c---->%c\n", x, z); } else { move(n-1, x, z, y); //将n-1个盘子从x 借助 z 移动到y 上 printf("%c---->%c\n", x, z); //将第n个盘资从x移动到z move(n-1, y, x, z); //将n-1个盘资从y 借助 x 移动到z上 } } int main() { int n; printf("请输入汉诺塔的层数: "); scanf("%d", &n); printf("移动的步骤如下:\n"); move(n, 'X', 'Y', 'Z'); return 0; }
八皇后问题
回溯算法的典型例题
描述:8x8棋盘上,放8个皇后棋子,每一行列斜线都不能有第二个棋子。
//递归实现 #include <stdio.h> #include <stdlib.h> int count; int notDanger(int row, int j, int (*chess)[8]) { int i, k, flag1=0, flag2=0, flag3=0, flag4=0, flag5=0; //判断方向 for(i=0; i < 8; i++) { if(*(*(chess+i)+j) != 0) { flag1 = 1; break; } } //判断左上方 for( i=row, k=j; i>=0&&k>=0; i--, k--) { if(*(*(chess+i)+k) != 0) { flag2 = 1; break; } } //判断右下方 for( i=row, k=j; i<8&&k<8; i++, k++) { if(*(*(chess+i)+k) != 0) { flag3 = 1; break; } } //判断右上方 for( i=row, k=j; i>=0&&k<8; i--, k++) { if(*(*(chess+i)+k) != 0) { flag4 = 1; break; } } //判断左下方 for( i=row, k=j; i<8&&k>=0; i++, k--) { if(*(*(chess+i)+k) != 0) { flag5 = 1; break; } } if( flag1 || flag2 || flag3 || flag4 || flag5) { return 0; } else { return 1; } } // 参数row: 表示起始行 // 参数n: 表示列数 // 参数(*chess)[8]:波安排时间哦指向棋盘每一行的指针 void EightQueen(int row, int n, int (*chess)[8]) { int chess2[8][8], i, j; for( i=0; i<8; i++ ) { for( j=0; j<8; j++ ) { chess2[i][j] = chess[i][j]; } } if( 8 == row) { printf("第%d 种", count+1); for( i=0; i<8; i++ ) { for( j=0; j<8; j++ ) { printf("%d ", *(*(chess2+i)+j)); } printf("\n"); } printf("\n"); count++; } else { for( j=0; j<n; j++) { if(notDanger(row, j, chess)) //判断是否危险 { for( i=0; i<8; i++) { *(*(chess2+row)+i) = 0; } *(*(chess2+row)+j) = 1; EightQueen(row+1, n, chess2); } } } } int main() { int chess[8][8], i , j; for( i=0; i<8; i++) { for( j=0; j<8; j++) { chess[i][j] = 0; } } EightQueen(0, 8, chess); printf("总共有%d种解法!\n\n", count); return 0; }
字符串
定义:串(String)是由零个或多个字符组成的有限序列,又名叫字符串
一般记为 s = “a1a2a3......an” (n>0)
串可以是空串,即没有字符,直接由“”表示。或者可以用希腊字母Φ表示
子串与主串
-
字符串比较大小:比较字符串内每个字符的ASCII码大小
-
字符串匹配
字符串的存储结构【同线性表】
- 顺序存储结构
- 一组地址连续的存储单元来存储串中的字符序列
- 按照预定义的大小,为每个定义的字符串变量分配一个固定长度的存储区,一般用定长数组来定义。
- 链式存储结构
BF(Brute Force)
属于朴素的模式匹配算法,核心思想:
- 有两个字符串S和T,长度为N和M。 - 首先S[1]和T[1]比较,若相等,则再比较S[2]和T[2],一直到T[M]为止;若不相等,则T向右移动一个字符的位置,再依次进行比较。 - 该算法最坏的情况下要进行M*(n-M+1)次比较,时间复杂度为`O(MN)`
字串定位操作称为串的模式匹配
KMP
回溯
#include <stdio.h> typedef char* String; void get_next(String T, int *next) { int i = 0; //后缀 int j = 1; //前缀 next[1] = 0; //第一个字符 while( i < T[0]) { i++; j++; //next[i] = j; if(T[i] != T[j]) //优化前缀字符重复时,修改next { next[i] = j; } else { next[i] = next[j]; } } else { j = next[j]; } } //返回子串T在主串S第pos个字符之后的位置 //若不存在,则返回0 int Inde_KMP(String S, String T, int pos) { int i = pos; int j = 1; int next[255]; get_next(T, next); while(0 == j || i <= S[0] && j <= T[0]) { if(s[i] == T[j]) { i++; j++; } else { j = next[j]; } } if( j > T[0]) //匹配成功 { return i - T[0]; } else //匹配失败 { return 0; } }
本文来自博客园,作者:Theseus‘Ship,转载请注明原文链接:https://www.cnblogs.com/yongchao/p/17150015.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南