赞
踩
数据结构是一种具有一定逻辑关系,在计算机中应用某种存储结构,并且封装了相应操作的数据元素的集合。
数据结构是在内存中操作数据,数据库是在外存中操作数据。
- 线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
- 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表可以分为:
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
typedef int SLDataType; // 顺序表的动态存储 typedef struct SeqList { SLDataType* array; // 指向动态开辟的数组 size_t size ; // 有效数据个数 size_t capicity ; // 容量空间的大小 }SeqList; // 基本增删查改接口 // 顺序表初始化 void SeqListInit(SeqList* psl); // 检查空间,如果满了,进行增容 void CheckCapacity(SeqList* psl); // 顺序表尾插 void SeqListPushBack(SeqList* psl, SLDataType x); // 顺序表尾删 void SeqListPopBack(SeqList* psl); // 顺序表头插 void SeqListPushFront(SeqList* psl, SLDataType x); // 顺序表头删 void SeqListPopFront(SeqList* psl); // 顺序表查找 int SeqListFind(SeqList* psl, SLDataType x); // 顺序表在pos位置插入x void SeqListInsert(SeqList* psl, size_t pos, SLDataType x); // 顺序表删除pos位置的值 void SeqListErase(SeqList* psl, size_t pos); // 顺序表销毁 void SeqListDestory(SeqList* psl); // 顺序表打印 void SeqListPrint(SeqList* psl);
void SeqListInit(SeqList* ps) { assert(ps); ps->a = NULL; ps->size = 0; ps->capacity = 0; } void SeqListDestroy(SeqList* ps) { assert(ps); free(ps->a); ps->a = NULL; ps->size = ps->capacity = 0; } void SeqListPrint(SeqList* ps) { assert(ps); for (size_t i = 0; i < ps->size; ++i) { printf("%d ", ps->a[i]); } printf("%\n"); } void CheckCacpity(SeqList* ps) { if (ps->size == ps->capacity) { size_t newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; ps->a = (SLDateType*)realloc(ps->a, newcapacity*sizeof(SLDateType)); ps->capacity = newcapacity; } } // 以下几个接口先讲不复用Insert和Erase的实现,最后再讲复用实现 void SeqListPushBack(SeqList* ps, SLDateType x) { //assert(ps); //CheckCacpity(ps); //ps->a[ps->size] = x; //ps->size++; SeqListInsert(ps, ps->size, x); } void SeqListPushFront(SeqList* ps, SLDateType x) { assert(ps); /*CheckCacpity(ps); size_t end = ps->size; while (end > 0) { ps->a[end] = ps->a[end - 1]; --end; } ps->a[0] = x; ++ps->size;*/ SeqListInsert(ps, 0, x); } void SeqListPopFront(SeqList* ps) { assert(ps); //size_t start = 0; //while (start < ps->size-1) //{ // ps->a[start] = ps->a[start + 1]; // ++start; //} //size_t start = 1; //while (start < ps->size) //{ // ps->a[start-1] = ps->a[start]; // ++start; //} //--ps->size; SeqListErase(ps, 0); } void SeqListPopBack(SeqList* ps) { assert(ps); //ps->a[ps->size - 1] = 0; //ps->size--; SeqListErase(ps, ps->size-1); } int SeqListFind(SeqList* ps, SLDateType x) { for (size_t i = 0; i < ps->size; ++i) { if (ps->a[i] == x) { return i; } } return -1; } // 顺序表在pos位置插入x void SeqListInsert(SeqList* ps, size_t pos, SLDateType x) { assert(ps); assert(pos <= ps->size); CheckCacpity(ps); //int end = ps->size - 1; //while (end >= (int)pos) //{ // ps->a[end + 1] = ps->a[end]; // --end; //} size_t end = ps->size ; while (end > pos) { ps->a[end] = ps->a[end - 1]; --end; } ps->a[pos] = x; ps->size++; } // 顺序表删除pos位置的值 void SeqListErase(SeqList* ps, size_t pos) { assert(ps && pos < ps->size); //size_t start = pos; //while (start < ps->size-1) //{ // ps->a[start] = ps->a[start + 1]; // ++start; //} size_t start = pos+1; while (start < ps->size) { ps->a[start-1] = ps->a[start]; ++start; } ps->size--; }
问题:
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
思考:如何解决以上问题呢?下面给出了链表的结构来看看。
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1.无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2.带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
// 1、无头+单向+非循环链表增删查改实现 typedef int SLTDateType; typedef struct SListNode { SLTDateType data; struct SListNode* next; }SListNode; // 动态申请一个结点 SListNode* BuySListNode(SLTDateType x); // 单链表打印 void SListPrint(SListNode* plist); // 单链表尾插 void SListPushBack(SListNode** pplist, SLTDateType x); // 单链表的头插 void SListPushFront(SListNode** pplist, SLTDateType x); // 单链表的尾删 void SListPopBack(SListNode** pplist); // 单链表头删 void SListPopFront(SListNode** pplist); // 单链表查找 SListNode* SListFind(SListNode* plist, SLTDateType x); // 单链表在pos位置之后插入x //为什么不在pos位置之前插入? //因为会大大降低链表的效率! void SListInsertAfter(SListNode* pos, SLTDateType x); // 单链表删除pos位置之后的值 // 为什么不删除pos位置? // 同上 void SListEraseAfter(SListNode* pos);
SListNode* BuySListNode(SLTDataType x) { SListNode* newnode = (SListNode*)malloc(sizeof(SListNode)); if (newnode == NULL) { perror("BuySListNode"); exit(-1); } newnode->data = x; newnode->next = NULL; return newnode; } void SListPrint(SListNode* plist) { while (plist != NULL) { printf("%d->", plist->data); plist = plist->next; } printf("NULL\n"); } void SListPushFront(SListNode** pplist, SLTDataType x) { assert(pplist); SListNode* newnode = BuySListNode(x); newnode->next = *pplist; *pplist = newnode; } void SListPushBack(SListNode** pplist, SLTDataType x) { assert(pplist); if (*pplist == NULL) { SListPushFront(pplist, x); } else { SListNode* ptr = *pplist; SListNode* newnode = BuySListNode(x); while (ptr->next != NULL) { ptr = ptr->next; } ptr->next = newnode; } } void SListPopBack(SListNode** pplist) { assert(pplist); assert(*pplist); if ((*pplist)->next == NULL) { SListPopFront(pplist); return; } SListNode* ptr = *pplist; SListNode* del = NULL; while (ptr->next != NULL) { del = ptr; ptr = ptr->next; } free(ptr); del->next = NULL; } void SListPopFront(SListNode** pplist) { assert(pplist); assert(*pplist); SListNode* del = *pplist; (*pplist) = (*pplist)->next; free(del); del = NULL; } SListNode* SListFind(SListNode* plist, SLTDataType x) { assert(plist); while (plist != NULL) { if (plist->data == x) return plist; plist = plist->next; } return NULL; } void SListInsertAfter(SListNode* pos, SLTDataType x) { assert(pos); SListNode* newnode = BuySListNode(x); newnode->next = pos->next; pos->next = newnode; } void SListInsert(SListNode** pplist, SListNode* pos, SLTDataType x) { assert(pplist); assert(pos); if (*pplist == pos) { SListPushFront(pplist, x); return; } SListNode* ptr = *pplist; while(ptr->next != NULL) { if (ptr->next == pos) { SListNode* newnode = BuySListNode(x); ptr->next = newnode; newnode->next = pos; return; } ptr = ptr->next; } } void SListEraseAfter(SListNode* pos) { assert(pos); assert(pos->next); SListNode* del = pos->next; pos->next = pos->next->next; free(del); del = NULL; } void SListErase(SListNode** pplist, SListNode* pos) { assert(pplist); assert(pos); assert(*pplist); if (*pplist == pos) { SListPopFront(pplist); return; } SListNode* ptr = *pplist; while (ptr->next != NULL) { if (ptr->next == pos) { SListNode* del = pos; ptr->next = pos->next; free(del); del = NULL; return; } ptr = ptr->next; } } void SListDestroy(SListNode** plist) { assert(plist); while (*plist) { SListNode* del = *plist; *plist = (*plist)->next; free(del); } }
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持O(1) | 不支持:O(N) |
任意位置插入或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存利用率 | 高 | 低 |
备注:缓存利用率参考存储体系结构 以及 局部原理性。
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
栈的实现一般可以使用数组或者链表实现,相对而言数组的结构实现更优一些。因为数组在尾上插入数据的代价比较小。
// 下面是定长的静态栈的结构,实际中一般不实用,所以我们主要实现下面的支持动态增长的栈 typedef int STDataType; #define N 10 typedef struct Stack { STDataType _a[N]; int _top; // 栈顶 }Stack; // 支持动态增长的栈 typedef int STDataType; typedef struct Stack { STDataType* _a; int _top; // 栈顶 int _capacity; // 容量 }Stack; // 初始化栈 void StackInit(Stack* ps); // 入栈 void StackPush(Stack* ps, STDataType data); // 出栈 void StackPop(Stack* ps); // 获取栈顶元素 STDataType StackTop(Stack* ps); // 获取栈中有效元素个数 int StackSize(Stack* ps); // 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 int StackEmpty(Stack* ps); // 销毁栈 void StackDestroy(Stack* ps);
void StackInit(Stack* ps) { assert(ps); ps->data = NULL; ps->top = 0; ps->capacity = 0; } void StackPush(Stack* ps, STDataType data) { assert(ps); if (ps->top == ps->capacity) { ps->capacity = (ps->capacity == 0) ? 4 : 2 * ps->capacity; STDataType* newdata = (STDataType*)realloc(ps->data, sizeof(STDataType) * ps->capacity); if (newdata == NULL) { perror("realloc fail"); exit(-1); } ps->data = newdata; } ps->data[ps->top] = data; ps->top++; } void StackPop(Stack* ps) { assert(ps); ps->top--; } STDataType StackTop(Stack* ps) { assert(ps); return ps->data[ps->top-1]; } int StackSize(Stack* ps) { assert(ps); return ps->top; } int StackEmpty(Stack* ps) { assert(ps); return !ps->top; } void StackDestroy(Stack* ps) { assert(ps); free(ps->data); ps->data = NULL; ps->top = 0; ps->capacity = 0; }
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:
进行插入操作的一端称为队尾
出队列:进行删除操作的一端称为队头
队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。
// 链式结构:表示队列 typedef struct QListNode { struct QListNode* _pNext; QDataType _data; }QNode; // 队列的结构 typedef struct Queue { QNode* _front; QNode* _rear; }Queue; // 初始化队列 void QueueInit(Queue* q); // 队尾入队列 void QueuePush(Queue* q, QDataType data); // 队头出队列 void QueuePop(Queue* q); // 获取队列头部元素 QDataType QueueFront(Queue* q); // 获取队列队尾元素 QDataType QueueBack(Queue* q); // 获取队列中有效元素个数 int QueueSize(Queue* q); // 检测队列是否为空,如果为空返回非零结果,如果非空返回0 int QueueEmpty(Queue* q); // 销毁队列 void QueueDestroy(Queue* q);
void QueueInit(Queue* q) { assert(q); q->front = NULL; q->rear = NULL; } void QueuePush(Queue* q, QDataType data) { assert(q); QNode* newnode = (QNode*)malloc(sizeof(QNode)); if (newnode == NULL) { perror("malloc fail"); exit(-1); } newnode->next = NULL; newnode->data = data; if (q->rear == NULL) { q->front = q->rear = newnode; } else { q->rear->next = newnode; q->rear = newnode; } } void QueuePop(Queue* q) { assert(q); assert(q->front); QNode* del = q->front; q->front = q->front->next; free(del); if (q->front == NULL) { q->rear = NULL; } } QDataType QueueFront(Queue* q) { assert(q); assert(q->front); return q->front->data; } QDataType QueueBack(Queue* q) { assert(q); assert(q->rear); return q->rear->data; } int QueueEmpty(Queue* q) { assert(q); if (q->front == NULL) return 1; else return 0; } void QueueDestroy(Queue* q) { assert(q); assert(q->front); QNode* cur = q->front; while (cur) { QNode* del = cur; free(del); cur = cur->next; } q->rear = NULL; }
另外扩展了解一下,实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。环形队列可以使用数组实现,也可以使用循环链表实现。
这里有一个问题:如何判断一个循环是否为空?
有两种方案:
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。
把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
- 有一个特殊的结点,称为根结点,根节点没有前驱结点
- 除根节点外,**其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,**其中每一个集合Ti(1<= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。
- 因此,树是递归定义的。
注意:树形结构中,子树之间不能有交集,否则就不是树形结构
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
一棵二叉树是结点的一个有限集合,该集合:
- 或者为空
- 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
从上图可以看出:
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
1. 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2. 链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链。
typedef int BTDataType; // 二叉链 struct BinaryTreeNode { struct BinTreeNode* _pLeft; // 指向当前节点左孩子 struct BinTreeNode* _pRight; // 指向当前节点右孩子 BTDataType _data; // 当前节点值域 } // 三叉链 struct BinaryTreeNode { struct BinTreeNode* _pParent; // 指向当前节点的双亲 struct BinTreeNode* _pLeft; // 指向当前节点左孩子 struct BinTreeNode* _pRight; // 指向当前节点右孩子 BTDataType _data; // 当前节点值域 }
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
顺序结构的详解在下文的堆中展开
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType _data;
struct BinaryTreeNode* _left;
struct BinaryTreeNode* _right;
}BTNode;
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。
设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: <= 且 <= ( >= 且 >= ) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。
向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
int array[] = {27,15,19,18,28,34,65,49,25,37};
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。
建堆有两种方法:
这两种方案哪个更好呢?
答案是从树的倒数第二层向下建堆
众所周知,二叉树的层数越高,该层的节点数也就越多,第二种方案中省去了调整二叉树最后一层的时间,就使得该方案的效率大大提高了。
typedef int HPDataType; typedef struct Heap { HPDataType* _a; int _size; int _capacity; }Heap; // 堆的构建 void HeapCreate(Heap* hp, HPDataType* a, int n); // 堆的销毁 void HeapDestory(Heap* hp); // 堆的插入 void HeapPush(Heap* hp, HPDataType x); // 堆的删除 void HeapPop(Heap* hp); // 取堆顶的数据 HPDataType HeapTop(Heap* hp); // 堆的数据个数 int HeapSize(Heap* hp); // 堆的判空 int HeapEmpty(Heap* hp);
void Swap(HPDataType* x1, HPDataType* x2) { HPDataType x = *x1; *x1 = *x2; *x2 = x; } void AdjustDown(HPDataType* a, int n, int root) { int parent = root; int child = parent*2+1; while (child < n) { // 选左右孩纸中大的一个 if (child+1 < n && a[child+1] > a[child]) { ++child; } //如果孩子大于父亲,进行调整交换 if(a[child] > a[parent]) { Swap(&a[child], &a[parent]); parent = child; child = parent*2+1; } else { break; } } } void AdjustUp(HPDataType* a, int n, int child) { int parent; assert(a); parent = (child-1)/2; //while (parent >= 0) while (child > 0) { //如果孩子大于父亲,进行交换 if (a[child] > a[parent]) { Swap(&a[parent], &a[child]); child = parent; parent = (child-1)/2; } else { break; } } } void HeapInit(Heap* hp, HPDataType* a, int n) { int i; assert(hp && a); hp->_a = (HPDataType*)malloc(sizeof(HPDataType)*n); hp->_size = n; hp->_capacity = n; for (i = 0; i < n; ++i) { hp->_a[i] = a[i]; } // 建堆: 从最后一个非叶子节点开始进行调整 // 最后一个非叶子节点,按照规则: (最后一个位置索引 - 1) / 2 // 最后一个位置索引: n - 1 // 故最后一个非叶子节点位置: (n - 2) / 2 for(i = (n-2)/2; i >= 0; --i) { AdjustDown(hp->_a, hp->_size, i); } } void HeapDestory(Heap* hp) { assert(hp); free(hp->_a); hp->_a = NULL; hp->_size = hp->_capacity = 0; } void HeapPush(Heap* hp, HPDataType x) { assert(hp); //检查容量 if (hp->_size == hp->_capacity) { hp->_capacity *= 2; hp->_a = (HPDataType*)realloc(hp->_a, sizeof(HPDataType)*hp->_capacity); } //尾插 hp->_a[hp->_size] = x; hp->_size++; //向上调整 AdjustUp(hp->_a, hp->_size, hp->_size-1); } void HeapPop(Heap* hp) { assert(hp); //交换 Swap(&hp->_a[0], &hp->_a[hp->_size-1]); hp->_size--; //向下调整 AdjustDown(hp->_a, hp->_size, 0); } HPDataType HeapTop(Heap* hp) { assert(hp); return hp->_a[0]; } int HeapSize(Heap* hp) { return hp->_size; } int HeapEmpty(Heap* hp) { return hp->_size == 0 ? 0 : 1; } void HeapPrint(Heap* hp) { int i; for (i = 0; i < hp->_size; ++i) { printf("%d ", hp->_a[i]); } printf("\n"); }
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
具体的代码实现将在后续的排序总结中涉及
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。