数据结构与算法(一)
-
推到大O阶(时间复杂度)方法:
- 用常数1取代运行时间中的所有加法常数
- 在修改后的运行次数函数中,只保留最高阶项
- 如果最高阶项存在且不是1,则去除与这个项相乘的常数
-
得到的最后结果就是大O阶
-
常见的时间复杂度
例子 时间复杂度 术语 520 O(1) 常数阶 3n+4 O(n) 线性阶 3n^2+4n+5 O(n^2) 平方阶 3log(2)n+4 O(logn) 对数阶 2n+3nlog(2)n+14 O(nlogn) nlogn阶 n^3+ 2n^2 + 4n + 6 O(n^3) 立方阶 2^n O(2^n) 指数阶 -
常用的时间复杂度所耗费的时间从小到大依次是:O(1) < O(logn) < (n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
-
线性表(List):由零个或者多个数据元素组成的有限序列
- 元素之间是有顺序的
- 若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他元素都有且只有一个前驱和后继
- 线性表是有限的
-
抽象数据类型:
-
ADT 线性表(List)
-
操作
- 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的元素个数
-
示例代码
//并集代码 //La表示A集合,Lb表示B集合 void unionL(list *La, list Lb) { int La_len, Lb_len, i; ElemType e; La_len = ListLength(*La); Lb_len = ListLength(Lb); for(i = 1; i <= Lb_len; i++) { GetElem(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; //顺序存储结构具有随机结构的特点,时间复杂度为O(1)
-
顺序存储结构封装需要三个属性:
- 存储空间的起始位置,数组data,它的存储位置就是线性表存储空间的存储位置
- 线性表的最大存储容量:数组长度MaxSize
- 线性表的当前长度:length
- 数组的长度初始化后一般不变,线性表的当前长度会改变
-
获取元素操作
#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个位置,分别将它们都向后移动一个位置; 将要插入元素填入位置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-1; 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) //当i不在范围内时 { 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)。适合元素个数比较稳定,不经常插入和删除元素,更多是存取数据的应用
-
顺序结构的优缺点
- 优点:
- 无须为表示表中元素之间的逻辑关系而增加额外的存储空间
- 可以快速地存取表中任意位置的元素
- 缺点:
- 插入和删除操作需要移动大量元素
- 当线性表长度变化较大时,难以确定存储空间的容量
- 容易造成存储空间的“碎片”
- 优点:
-
-
单链表
-
单链表存储结构
typedef struct Node { ElemType data; //数据域 struct Node* Next; //指针域 }Node; typedef struct Node* LinkList; //结点由存放数据元素的数据域和存放后继结点地址的指针域组成
-
单链表的读取
/*************************************** 获得链表第i个数据的算法思路: 声明一个结点p指向链表第一个结点,初始化j从1开始 当j < i 时,就遍历链表,让p的指针向后移动,不断指向下一个结点, j+1 若到链表末尾p为空,则说明第i个元素不存在 否则查找成功,返回结点p的数据 ****************************************/ Status GetElem( LinkList 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; }
-
单链表的插入
/*********************************************** 单链表第i个数据插入结点的算法思路: 声明一结点p指向链表头结点,初始化j从1开始 当j<1时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1 若到链表末尾p为空,则说明第i个元素不存在; 否则查找成功,在系统中生成一个空结点s 将数据元素e赋值给s->data 单链表的插入刚才两个标准语句 返回成功 ***********************************************/ Status ListInsert( LinkList *L, int i, ElemType *e )//LinkList是结构体的指针,指针做形参时都要加*表示是指针 { int j; LinkList p, a; 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删除单链表的操作,其实就是将它的前继结点的指针绕过指向后继结点即可
/*********************************** 单链表第i个数据删除结点的算法思路: 声明结点p指向链表第一个结点,初始化j=1 当j<1时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1 若到链表末尾p为空,则说明第i个元素不存在 否则查找成功,将欲删除结点p->next赋值给q 单链表的删除标准语句p->next = q->next 将q结点中的数据赋值给e,作为返回 释放q结点 ************************************/ Status ListDelete(LinkList *L, int i, ElemType *e) { int j; LinkList p,q; p = *L; j = 1; while(p->next && 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; }
-
单链表的创建
/************************************* 声明一结点p和计数器变量i 初始化一空链表L 让L的头结点的指针指向NULL,即建立一个带头结点的单链表 循环实现后继结点的赋值和插入 **************************************/ //头插法 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; p->next = (*L)->next; (*L)->next = p; } } //尾插法 void CreateListTail(LinkList *L, int n) { LinkList p, r; int i; srand(time(0)); *L = (LinkList)malloc(sizeof(Node)); r = *L; for(i = 0; i < n; i++ ) { p = (Node *)malloc(sizeof(Node)); p->data = rand()% 100 + 1; r->next = p; r = p; } r->next = NULL; }
-
单链表整表删除
/****************************** 声明结点p和q 将第一个结点赋值给p,下一个结点赋值给q 循环执行释放p和将q赋值给p的操作 *******************************/ Status ClearList(LinkList *L) { LinkList p, q; p = (*L) ->next; while(p) { q = p->next; free(p); p = q; } (*L)->next = NULL; return OK; }
-
-
单链表结构与顺序存储结构优缺点:
- 存储分配方式:
- 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素。
- 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素。
- 时间性能:
- 查找
- 顺序存储结构O(1)
- 单链表O(n)
- 插入和删除
- 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
- 单链表在计算出某位置的指针后,插入和删除时间仅为O(1)
- 查找
- 空间性能:
- 顺序存储结构需要预分配存储空间,分大了容易造成空间浪费,分小了容易发生溢出
- 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制
- 结论:
- 若线性表需要频繁查找,很少进行插入和删除操作是,宜采用顺序存储结构
- 若需要频繁插入和删除时,宜采用单链表结构
- 存储分配方式:
-
-