数据结构--程序员的基本功
作为一个非科班出身的我,即便是在 iOS 上再努力,可还是觉得比起计算机专业的人缺少点什么. 嗯 经过长时间的思考,可能就是缺乏经过计算机专业知识熏陶的的某种素养吧. 那就抽时间来学习《数据结构与算法》吧, 本书读完后,会继续攻读网络部分.
本文章也仅仅适合非科班出身入门级别的,在此也作为本人的学习记录.
文章中的代码地址 LLDataStructures
文章目录
- 1 什么是数据结构
- 2 算法的时间复杂度,空间复杂度
- 3 线性表
- 4 栈与队列
- 5 二叉树
- 6 常见的算法题
一 什么是数据结构
数据结构:是相互之间存在一种或多种特定关系的数据元素的集合
1.1 逻辑结构
逻辑结构: 是指数据对象中数据元素之间的相互关系.
逻辑结构也分为以下四种
-
1.1.1 集合结构
集合结构:集合结构中的数据元素除了同属于一个集合外,他们之间没有其他关系
集合结构.png -
1.1.2 线性结构
线性结构中的数据元素之间是一对一的关系
image.png -
1.1.3 树形结构
树形结构中的数据元素之间存在一种一对多的层次关系
树形结构.png
-
1.1.4 图形结构
图形结构的数据元素是多对多的关系
图形结构.png
1.2物理结构
物理结构 是指数据的逻辑结构在计算机中的存储形式
-
1.2.1 顺序存储结构
是把数据元素存放在地址连续的存储单元里,在数据间的逻辑关系和物理关系是一致的
顺序存储结构.png - 1.2.2 链式存储结构
链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的
二 算法
2.1 算法的时间复杂度
在进行算法分析时, 语句总的执行次数 T(n)是关于问题规模n 的函数,进而分析 T(n)随n的变化情况并确定 T(n)的数量级.算法的时间复杂度,也就是算法的时间量度,记做 T(n) = O(f(n)) 他表示随问题的规模 n 的增大, 算法执行时间的增长率和 f(n)的增长率相同, 称作算法的渐进时间复杂度,简称时间复杂度. 其中 f(n)是问题规模 n 的某个函数.
这样用大写 O()来体现算法时间复杂度的记法,我们称之为 大 O 记法
2.2 推导 大 O阶方法
- 用常数1取代运行时间中的所有加法常数.
- 在修改后的运行次数函数中,只保留最高阶项
- 如果最高阶项存在切不是1,则去除与这个项相乘的常数.
得到的结果就是 大 O阶
2.3 常数阶
sum = (1 + n)*n/2
上述算法语句的时间复杂度就是 O(1)
2.4 线性阶
int i;
for(i = 0; i < n; i++){
/*时间复杂度为 O(1)的程序步骤序列*/
}
上述的算法时间复杂度为 O(n) 因为循环体中的代码要执行 n 次
2.5 对数阶
int count = 1;
while (count < n) {
count = count * 2;
/*时间复杂度为 O(1)的程序步骤序列*/
}
由于每次 count 乘以2 以后,就距离 n 更近一份,也就是说,有多少个2相乘后大于 n, 则会退出循环,由 2^x = n 得到 x = log2n (其中2是底数) 所以这个循环的时间复杂度为 O(logn);
2.6 平方阶
int i,j;
for (i = 0; j<n;j++){
for(j = 0;j<m;j++){
/*时间复杂度为 O(1)的程序步骤序列*/
}
}
内层时间复杂度为 O(m),外层时间复杂度为 O(n),则改算法的时间复杂度为 O(m*n);
2.7 算法的空间复杂度
算法的空间复杂度通过计算算法所需的存储空间,算法空间复杂度的计算公式记作: S(n) = O(f(n)), 其中 n 为问题的规模, f(n)为语句关于 n 所占存储空间的函数;
通常,我们都是用"时间复杂度"来指运行时间的需求,使用"空间复杂度"指空间需求,当不用限定词的使用"复杂度"时,通常都是指时间复杂度
在文章最后会详细讲解几种常用的算法题以及他们的时间复杂度
三 线性表
线性表:零个或多个数据元素的有限序列
3.1 线性表的顺序存储结构
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素
来看一下线性表的顺序存储的结构代码
#define MAXSIZE 20
typedef int ElemtType;
typedef struct Sqlist{
ElemtType data[MAXSIZE]; // 数组存取数据元素, 最大值为 MAXSIZE
int length; // 线性表当前长度
}Sqlist;
从上边看出 顺序存储结构需要三个属性
- 存储空间的起始位置:数组Data 他的存储位置就是存储空间的存储位置
- 线性表的最大存储容量:数组长度MaxSize
- 线性表的当前长度:length
线性表的长度是线性表中数据元素的个数, 随着线性表插入和删除操作的进行,这个量是变化的。
在任意时刻,线性表的长度应该小于等于数组的长度
3.1.1 线性表插入元素 (思路如下)
- 如果插入位置不合理,抛出异常
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量
- 从最后一个元素开始向前遍历到第i个位置,分别将他们都向后移动一个位置
- 将要插入元素填入位置i处
- 表长加 1
// 向顺序线性表中插入一个元素 插入算法
Status ListInsert(Sqlist *L,int i,ElemtType e){
int k;
if (L->length == MAXSIZE) {
return ERROR;
}
if (i < 1 || i > L->length+1) {
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;
}
3.1.2 删除操作(思路如下)
- 如果删除位置不合理,抛出异常
- 取出删除元素
- 从删除元素位置开始遍历到最后一个元素位置,分别将他们都向前移动一个位置。
- 表长减 1
// 向顺序线性表中删除一个元素 删除算法
Status ListDelete(Sqlist *L,int i,ElemtType e){
int k;
if (L->length == 0) return ERROR;
if (i < L->length || 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;
}
顺序存储线性表优缺点
优点 | 缺点 |
---|---|
无需为表示表中元素之间逻辑关系而增加额外的存储空间 | 插入和删除操作需要移动大量元素 |
可以快速的存取表中任意位置的元素 | 当线性表长度变化较大时,难以确定存储空间的容量 |
造成存储空间的浪费 |
3.2 线性表的链式存储结构
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组数据元素单元可以是连续的,也可以是不连续的,这就意味着这些数据元素可以存在内存未被占用的任意位置
线性表的链式存储的结构代码
typedef int LLElemType;
typedef struct LLLinkNode {
LLElemType elem;
struct LLLinkNode *next;
}LLLinkNode;
结点由存放数据元素的数据域和存放后继结点地址的指针域组成。
- 3.2.1 单链表的创建
// 创建链表
- (void)creatLinkChain {
_link = initLinkNode();
LLLinkNode *p = _link;
for (int i = 0; i <= 10; i++) {
LLLinkNode *node = malloc(sizeof(LLLinkNode));
node->elem = i;
node->next = NULL;
p->next = node;
p = node;
}
}
- 3.2.2 单链表的删除
- (LLLinkNode *)deleteLinkChain:(LLLinkNode *)header {
LLLinkNode *p,*q;
p = header->next;
while (p) {
q = p->next;
free(p);
p = q;
}
header->next = NULL;
return header;
}
- 3.2.3 单链表的读取
// 打印链表
- (void)printLinkChain:(LLLinkNode *)p {
p = p->next;
while (p != NULL) {
NSLog(@"LinkChain:%d",p->elem);
p = p->next;
}
}
- 3.2.4 单链表中插入一个结点
- (LLLinkNode *)insertNode:(LLLinkNode *)header index:(int)i data:(int)data {
int j = 1;
LLLinkNode *p,*q;
p = header;
while (p && j < i) { // 寻找 第 i-1 个节点
p = p->next;
++j;
}
if (!p || j>i) {
return header; // 返回原链表
}
q = (LLLinkNode *)malloc(sizeof(LLLinkNode));// 生成新节点
q->elem = data;
q->next = p->next;
p->next = q;
return header;
}
- 3.2.5 单链表中 删除一个结点
- (LLLinkNode *)deleteNode:(LLLinkNode *)header index:(int)i data:(int)data {
int j = 1;
LLLinkNode *p,*q;
p = header;
while (p->next && j < i) {
p = p->next;
++j;
}
if ( !(p->next) || j > i) {
return header;
}
q = p->next;
p->next = q->next;
data = q->elem;
free(q);
return header;
}
- 3.2.6 递归法逆序链表
- (LLLinkNode *)recrusiveRevertChain:(LLLinkNode *)header {
LLLinkNode *current, *headerNext;
if (header == NULL || header->next == NULL) return header;
current = header;
headerNext = header->next;
header = [self recrusiveRevertChain:headerNext];
headerNext->next = current;
current->next = NULL;
return header;
}
- 3.2.7 尾插法 逆序链表
- (LLLinkNode *)revertChain:(LLLinkNode *)link {
LLLinkNode *p = link;
p = p->next;
link->next = NULL;
while (p != NULL) {
LLLinkNode *q = p;
p = p->next;
q->next = link->next;
link->next = q;
}
return link;
}
单链表结构与顺序存储结构优缺点
存储分配方式 | 时间性能 | 空间性能 | |
---|---|---|---|
顺序存储结构 | 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素 | 在查找方面:顺序存储O(1), 在插入和删除方面: 顺序存储结构需要平均移动表长一半的元素,时间为O(n) | 顺序存储结构需要预分配存储空间,分大了,浪费,分小了易发生上溢 |
链式存储结构 | 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素 | 在查找方面 单链表O(n); 在插入和删除方面: 单链表在线出某位置的指针后,插入和删除时间仅为O(1) | 单链表不需要分配存储空间,只要有就可以分配,元素个数不受限制 |
从上边表格的优缺点可以得出结论
- 若线性表需要频繁查找, 很少进行插入和删除操作时,宜采用顺序存储结构。需要频繁插入和删除时,宜采用单链表结构,
- 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构吗,这样可以不需要考虑存储空间的大小问题。如果事先知道线性表的大致长度,比如一年12个月, 一周就是7天,这种用顺序存储结构效率会高很多
3.3 双向链表
双向链表是在单链表的每个结点中,在设置一个指向其前驱结点的指针域
双向链表的代码结构
typedef int LLElemType;
typedef struct LLDulNode {
LLElemType data;
struct LLDulNode *prior; // 前驱指针
struct LLDulNode *next; // 后驱指针
}LLDulNode;
- 3.3.1创建双链表
- (void)creatLinkChain {
_linkNode = initDoubleLinkNode();
LLDulNode *p = _linkNode;
for (int i = 0; i<10; i++) {
LLDulNode *node = malloc(sizeof(LLDulNode));
node->data = i;
p->next = node;
node->prior = p;
node->next = _linkNode;
_linkNode->prior = node;
p = node;
}
}
- 3.3.2读取双链表
- (void)printLinkChain:(LLDulNode *)linkNode {
LLDulNode *p = linkNode;
p = p->next;
while (p != linkNode) {
NSLog(@"%zd",p->data);
p = p->next;
}
}
四 栈与队列
4.1 栈
栈是限定仅在表尾进行插入和删除操作的线性表
我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)不含任何数据元素的栈称为空栈,栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构
4.2 栈的顺序存储结构及实现
#define MAXSIZE 20
typedef int SElemtType;
// 节点的节点结构
typedef struct SqStack {
SElemtType data[MAXSIZE];
int top; // 用于栈顶指针
}SqStack;
- 4.2.1 栈的顺序存储结构 -- 进栈操作
// 进栈操作 插入元素e为新的栈顶元素
Status Push(SqStack *S,SElemtType e){
if (S->top == MAXSIZE - 1) { // 栈满了
return 0;
}
S->top++; // 栈顶指针增加1
S->data[S->top] = e; // 将新插入的元素赋值给栈顶空间
return 1;
}
- 4.2.2 栈的顺序存储结构 -- 出栈操作
// 出栈操作。 若栈不空,则删除S的栈顶元素,用e返回其值,并返回1 否则返回0
Status Pop(SqStack *S,SElemtType e) {
if (S->top == -1) {
return 0;
}
e = S->data[S->top]; // 将要删除的栈顶元素赋值给 e
S->top--; // 栈顶指针减一
return 1;
}
4.3 栈的链式存储结构及实现
栈的链式存储结构,简称为链栈
对于链栈来说,基本不存在栈满的情况,除非内存已经没有可以使用的空间了。
/*
栈的链式存储结构
*/
typedef struct StackNode {
SElemtType data;
struct StackNode *next;
}StackNode,LinkStackPtr;
typedef struct LinkStack {
LinkStackPtr top;
int count;
}LinkStack;
- 4.3.1 栈的链式存储结构 -- 进栈操作
// 链栈 插入新的元素e为新的栈顶元素
Status LinkPush(LinkStack *S,SElemtType e) {
LinkStackPtr p = (LinkStackPtr)malloc(sizeof(StackNode));
p->data = e;
p->next = S->top;// 把当前的栈顶元素赋值给新结点的直接后继,
S->top = p;// 将新的结点s赋值给栈顶指针
S->count++;
return 1;
}
-
4.3.2 栈的链式存储结构 -- 出栈操作
出栈pop 操作也是简单的三句话, 假设p用来存储要删除的栈顶结点,将栈顶指针下移一位,最后释放p即可
// 若栈不空,则删除S的栈顶元素,用e返回其值,并返回ok,否则返回error
Status LinkPop(LinkStack *S,SElemtType e) {
LinkStackPtr p;
if (!S) {
return 0;
}
e = S->top->data;
p = S->top; // 将栈顶结点赋值给p
S->top = S->top->next; // 使得栈顶指针下移一位,指向后一结点
free(p);// 释放结点p
S->count--;
return 1;
}
4.2 队列
队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表
队列是一种先进先出(First In First Out)的线性表,简称FIFO,允许插入的一端为对尾,允许删除的一端称为对头
- 4.2.1 队列的链式存储结构
typedef int QElemtType;
// 节点的节点结构
typedef struct QNode {
QElemtType data;
struct QNode *next;
}QNode,*QueuePtr;
// 队列的链表结构
typedef struct LLLinkQueue{ // 队头队尾指针
QueuePtr front;
QueuePtr rear;
}LLLinkQueue;
- 4.2.2 队列的链式存储结构---入队操作
入队操作时,其实就是在链表尾部插入节点
void enQueue(LLLinkQueue *queue, QElemtType elem) {
QueuePtr temp = malloc(sizeof(QNode));
if (temp && queue->rear) { // 分配空间成功
temp->data = elem;
temp->next = NULL;
queue->rear->next = temp;
}
}
- 4.2.3 队列的链式存储结构---出队操作
QElemtType deQueue(LLLinkQueue *q) {
if (q->rear == q->front) {
return 0;
}
QueuePtr temp = q->front->next;
if (q->front->next == q->rear) {
q->rear = q->front;
}
q->front->next = q->front->next->next;
return temp->data;
}
五 树
之前谈的都是一对一的线性结构, 可现实中,还有很多一对多的情况,那就引出了一对多的数据结构----- 树
二叉树.png5.1 二叉树的特点
- 每个结点最多有两棵树, 虽有二叉树中不存在度大于2的结点, 注意不是只有两棵树,而是最多有, 没有子树或者有一颗子树都是可以的
- 左子树和右子树是有顺序的, 次序不能任意颠倒
- 即使树中某结点只有一棵树,也要区分他是左子树还是右子树
比如下图左右两个是不同的二叉树
image.png
5.2 二叉树的链式存储结构
二叉树的每个结点最多有两个孩子,所以为他设计一个数据域和两个指针是比较自然地想法,叫做二叉链表
二叉链表的代码结构
typedef int QElemtType;
typedef struct LLNode{
QElemtType data;
struct LLNode *leftChild,*rightChild;
}LLNode,*LLTree;
5.3 遍历二叉树的原理
二叉树的遍历是指从根节点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次
- 先序遍历二叉树
// 先序遍历二叉树
- (void)preOrderTraverse:(LLTree)T {
if (T == NULL) return;
NSLog(@"%zd",T->data);// 显示结点数据, 可以更改为其他队结点操作
[self preOrderTraverse:T->firstChild];// 再先序遍历左子树
[self preOrderTraverse:T->rightSib]; // 最后先序遍历右子树
}
- 中序遍历二叉树
// 中序遍历二叉树
- (void)inOrderTraverse:(LLTree)T {
if (T == NULL) return;
[self preOrderTraverse:T->firstChild]; // 中序遍历左子树
NSLog(@"%zd",T->data); // 显示结点数据, 可以更改为其他队结点操作
[self preOrderTraverse:T->rightSib];// 最后中序遍历右子树
}
- 后序遍历二叉树
// 后序遍历二叉树
- (void)postOrderTraverse:(LLTree)T {
if (T == NULL) return;
[self preOrderTraverse:T->firstChild]; // 先后序遍历左子树
[self preOrderTraverse:T->rightSib];// 在后序遍历右子树
NSLog(@"%zd",T->data); // 显示结点数据, 可以更改为其他队结点操作
}
5.4 二叉树的查找
/**
二叉树的查找
@param T 二叉树
@param key 查找二叉树中的key
@param fatherT father指向T的双亲,最初设置为null
@param path 若查找成功, 则指针p指向该数据元素结点,并返回true
@return 否则指针p指向查找路径上访问的最后一个结点并返回false
*/
- (int)searchTree:(LLTree)T Key:(int)key fatherT:(LLTree)fatherT path:(LLTree)path {
if (!T) {
path = fatherT;
return false;
}else if (key == T->data) {
path = T;
return true;
}else if (key < T->data) {
return [self searchTree:T->firstChild Key:key fatherT:T path:path];
}else {
return [self searchTree:T->rightSib Key:key fatherT:T path:path];
}
}
5.5 二叉树的插入操作
/**
二叉树的插入
@param T 二叉树
@param key 插入Key
@return 成功返回true 失败返回false
*/
- (int)insertTree:(LLTree)T key:(int)key {
LLTree path,sert;
if (![self searchTree:T Key:key fatherT:NULL path:path]) { // 查找不成功
sert = malloc(sizeof(LLTree));
sert->data = key;
sert->firstChild = sert->rightSib = NULL;
if (!path) {
T = sert; // 插入s为新的跟结点
}else if (key < path->data) {
path->firstChild = sert; // 插入s为左结点
}else {
path->rightSib = sert; // 插入s为右结点
return true;
}
}
return false;
}
六 常见的算法题
6.1折半查找
/**
二分查找法 前提是有序数组
*/
- (void)binarySearch:(int)num {
NSArray *arr = @[@1,@2,@4,@6,@8,@9,@13,@23,@54,@64,@68];
int i = 0;
int j = ((int)arr.count - 1);
int index = -1;
while (i<=j) {
index = (j+i)/2;
if ([arr[index] intValue] == num) {
break;
}else if ([arr[index] intValue] > num){
j = index - 1;
}else{
i = index + 1;
}
}
if (i > j) {
index = -1;
};
NSLog(@"index = %d",index);
}
6.2 冒泡排序 时间复杂度O(n^2)
/**
冒泡排序
*/
- (void)bubbleSort {
for (int i = 0 ; i < self.arrM.count - 1 ; i++) {
for (int j = 0; j < self.arrM.count - 1 - i; j++) {
if (self.arrM[j] < self.arrM[j+1]) {
[self.arrM exchangeObjectAtIndex:j withObjectAtIndex:j+1];
}
}
}
NSLog(@"%@",self.arrM);
}
6.3 选择排序 时间复杂度O(n^2)
/**
选择排序
*/
- (void)selectSort {
for (int i = 0; i < self.arrM.count - 1; i++) {
for (int j = i+1; j < self.arrM.count -1; j++) {
if ([self.arrM[i]intValue] > [self.arrM[j]intValue]) {
[self.arrM exchangeObjectAtIndex:j withObjectAtIndex:i];
}
}
}
NSLog(@"%@",self.arrM);
}
6.4 插入排序 时间复杂度O(n^2)
/**
插入排序 前提是该数组已经是一个有序表
*/
- (void)insertSort {
int i,j;
for ( i = 2; i <= self.arrM.count - 1; i++) {
if ([self.arrM[i] intValue] < [self.arrM[i-1] intValue]) { // 需要将self.arrM[i-1]插入到有序表中
self.arrM[0] = self.arrM[i]; // 设置哨兵
for ( j = i - 1; [self.arrM[j] intValue] > [self.arrM[0] intValue]; j--) {
self.arrM[j+1] = self.arrM[j]; // 记录后移
}
self.arrM[j+1] = self.arrM[0]; // 插入到正确的位置
}
}
}
6.5 快速排序 时间复杂度O(nlogn)
/**
快排
*/
- (void)quickSortWithArrM:(NSMutableArray *)arrM left:(int)left right:(int)right {
int i,j,temp;
if (left > right) {
return;
}
temp = [arrM[left] intValue];
i = left;
j = right;
while (i < j) {
// 顺序很重要 一定要先从右往左找
while ([arrM[j] intValue] >= temp && i<j ) j--;
while ([arrM[i] intValue] <= temp && i<j) i++;
[arrM exchangeObjectAtIndex:i withObjectAtIndex:j];
}
// 最终将基准数归位
arrM[left] = arrM[i];
arrM[i] = @(temp);
[self quickSortWithArrM:arrM left:left right:i - 1];
[self quickSortWithArrM:arrM left:i + 1 right:right];
}