数据结构
程序 = 数据结构 + 算法
数据结构是计算机专业中一门综合性的基础课程,它是介于数学,计算机硬件和计算机软件的三者之间一门核心课程,同时,数据结构是设计数据库,程序,操作系统,游戏等等设计方面的重要基础,是绝大多数计算机专业考研的指定科目,也是大公司面试时常考科目,同时,也是高中及大学的学课竞赛中必备知识,优秀的数据结构和算法,可见数据结构在计算机课程中的重要性。
计算机的算法与数据结构密切相关,算法无不依赖于数据结构,而数据结构也关系到算法的效率,直接决定了一个程序的好坏。
一、顺序表
我们再来说一下数组,数组作为一个顺序储存方式数据结构为我们的程序设计带来了大量的便利,几乎任何的高级程序设计,算法设计都离不开数组的灵活使用,但是,数组最大的缺点就是我们的插入和删除时需要移动大量的元素,显然这需要消耗大量的时间。
我们在c语言中考虑一种最坏的情况,就是删除数组的第一个元素或者插入第一个位置,我们是不是都要对后面的数组进行一个大量的挪动啊,这种方式是不是很浪费资源啊,而且还要考虑到数组的大小,否则有溢出风险。
但是顺序表也有优点。
1.随机访问性能好:通过下标直接访问元素,时间复杂度为O(1)。
2.存储效率高:不需要额外的存储空间来存储指针,只需存储元素本身,相对于链表存储效率较高。
3.空间利用率高:连续存放数据元素,命中率高,局部性原理适用。
4.便于随机访问:支持常数时间复杂度的随机访问,通过索引可以直接获取元素值,无需遍历整个列表。
顺序表的代码实现如下:
//下面两种防止头文件被重复包含的写法都可以用
//#pragma once
#ifndef __SEQLIST__H__
#define __SEQLIST__H__
#include<stdio.h>
#include<string.h>
#include <stdlib.h>
#include <assert.h>
typedef int SQDataType;
// 动态的顺序表,满的话可以扩容。
typedef struct SeqList
{
SQDataType* a;
int size; // 有效数据的个数
int capacity; // 容量
}SL;
//typedef struct SeqList SL; 和上面结构体后面SL一个意思
//初始化
void SeqListInit(SL* ps);
//打印
void SeqListPrint(SL* ps);
//销毁,释放空间
void SeqListDestory(SL* ps);
//尾插
void SeqListPushBack(SL* ps, SQDataType x);
//头插
void SeqListPushFront(SL* ps, SQDataType x);
//尾删
void SeqListPopBack(SL* ps);
//尾插
void SeqListPopFront(SL* ps);
//选择一个合适的位置插入
void SeqListInsert(SL* ps, int pos, SQDataType x);
//选择某个位置进行删除
void SeqListErase(SL* ps, int pos);
//查找元素
int SeqListFind(SL* ps, SQDataType x);
//修改某个元素
void SeqListModity(SL* ps, int pos, SQDataType x);
#endif
#include "SeqList.h"
void SeqListInit(SL* ps)
{
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
void SeqListDestory(SL* ps)
{
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
void SeqListCheckCapacity(SL* ps)
{
// 检查一下是否满了,满了的话就进行扩容,这里选择的是二倍扩容
if (ps->size == ps->capacity)
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SQDataType* tmp = (SQDataType*)realloc(ps->a, newcapacity * sizeof(SQDataType));
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
else
{
ps->a = tmp;
ps->capacity = newcapacity;
}
}
}
void SeqListPushBack(SL* ps, SQDataType x)
{
//具体实现如下
//SeqListCheckCapacity(ps);
//ps->a[ps->size] = x;
//ps->size++;
SeqListInsert(ps, ps->size, x);
}
void SeqListPushFront(SL* ps, SQDataType x)
{
//具体实现如下
//SeqListCheckCapacity(ps);
//int end = ps->size - 1;
//while (end >= 0)
//{
// ps->a[end + 1] = ps->a[end];
// end--;
//}
//ps->a[0] = x;
//ps->size++;
SeqListInsert(ps, 0, x);
}
void SeqListPopBack(SL* ps)
{
//assert(ps->size > 0);
ps->a[ps->size - 1] = 0;
//ps->size--;
SeqListErase(ps, ps->size - 1);
}
void SeqListPopFront(SL* ps)
{
assert(ps->size > 0);
SeqListErase(ps, 0);
}
void SeqListInsert(SL* ps, int pos, SQDataType x)
{
assert(pos <= ps->size);
SeqListCheckCapacity(ps);
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
void SeqListErase(SL* ps, int pos)
{
assert(pos < ps->size);
int start = pos + 1;
while (start < ps->size)
{
ps->a[start-1] = ps->a[start];
start++;
}
ps->size--;
}
int SeqListFind(SL* ps, SQDataType x)
{
for (int i = 0; i < ps->size; ++i)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
void SeqListModity(SL* ps, int pos, SQDataType x)
{
assert(pos < ps->size);
ps->a[pos] = x;
}
void SeqListPrint(SL* ps)
{
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
二、单链表
链表的存储方式是不连续的。同时链表是自适应内存大小的,也就是说无论我们设多大的数据,理论上都可以实现的。
链表的基本思维是,利用结构体的设置,额外开辟出一份内存空间去作指针,它总是指向下一个结点,一个个结点通过NEXT指针相互联系,串联,这就形成了我们的链表。(下图所示)
我们看前面这幅图,是不是解决了我们在头插和头删的时候大量挪动数据啊。
总结一下单链表的优缺点:
优点:元素的存储单元是任意的,可连续也可不连续;不需要限定长度;动态内存分配;高效的插入和删除操作;节省内存;易于实现。
缺点:查找时间复杂度为O(n);存放元素时需要另外开辟一个指针域的空间;每个节点需要指针;遍历元素不够直观;空间浪费;适用场景有限。
单链表的代码实现如下:
#pragma once
#include <stdio.h>
#include <stdlib.h>
typedef int SLTDataType;
struct SListNode
{
SLTDataType data;
struct SListNode* next;
};
typedef struct SListNode SLTNode;
// 不会改变链表的头指针,传一级指针
void SListPrint(SLTNode* phead);
// 可能会改变链表的头指针,传二级指针
void SListPushBack(SLTNode** pphead, SLTDataType x);
void SListPushFront(SLTNode** pphead, SLTDataType x);
void SListPopFront(SLTNode** pphead);
void SListPopBack(SLTNode** pphead);
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
// 在pos的前面插入x
void SListInsert(SLTNode** phead, SLTNode* pos, SLTDataType x);
// 删除pos位置的值
void SListErase(SLTNode** phead, SLTNode* pos);
#include "SList.h"
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
SLTNode* BuySListNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySListNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SListPopFront(SLTNode** pphead)
{
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
void SListPopBack(SLTNode** pphead)
{
if (*pphead == NULL)
{
return;
}
else if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SListNode* cur = phead;
//while (cur != NULL)
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
if (pos == *pphead)
{
SListPushFront(pphead, x);
}
else
{
SLTNode* newnode = BuySListNode(x);
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
void SListErase(SLTNode** pphead, SLTNode* pos)
{
if (pos == *pphead)
{
SListPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
双向链表
可以观察一下双向链表和单链表的区别。
总结一下双向链表的优缺点:
优点:支持双向遍历,访问任意节点的时间复杂度为O(1);在中间插入和删除节点时更高效。
缺点:实现复杂度较高,需要维护两个方向的指针;需要额外的空间来存储指向前一个节点的指针。
双向链表代码实现如下:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int LTDataType;
// 带头双向循环 -- 最有链表结构,任意位置插入删除数据都是O(1)
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}ListNode;
ListNode* ListInit();
void ListDestory(ListNode* phead);
void ListPrint(ListNode* phead);
void ListPushBack(ListNode* phead, LTDataType x);
void ListPushFront(ListNode* phead, LTDataType x);
void ListPopFront(ListNode* phead);
void ListPopBack(ListNode* phead);
ListNode* ListFind(ListNode* phead, LTDataType x);
void ListInsert(ListNode* pos, LTDataType x);
void ListErase(ListNode* pos);
//判断是否为空
bool ListEmpty(ListNode* phead);
int ListSize(ListNode* phead);
#include "List.h"
ListNode* BuyListNode(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
ListNode* ListInit()
{
ListNode* phead = BuyListNode(0);
phead->next = phead;
phead->prev = phead;
return phead;
}
void ListDestory(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
ListNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
phead = NULL;
}
void ListPrint(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
void ListPushBack(ListNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead, x);
}
void ListPushFront(ListNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead->next, x);
}
void ListPopFront(ListNode* phead)
{
assert(phead);
ListErase(phead->next);
}
void ListPopBack(ListNode* phead)
{
assert(phead);
ListErase(phead->prev);
}
ListNode* ListFind(ListNode* phead, LTDataType x)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* newnode = BuyListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
栈
栈是一种先进后出的数据结构。
入栈示意图:
出栈示意图:
栈代码实现如下:
#include <stdio.h>
#include <stdbool.h>
#include <assert.h>
#include <stdlib.h>
typedef char STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void StackInit(ST* ps);
void StackDestory(ST* ps);
// 入栈
void StackPush(ST* ps, STDataType x);
// 出栈
void StackPop(ST* ps);
STDataType StackTop(ST* ps);
int StackSize(ST* ps);
bool StackEmpty(ST* ps);
void StackInit(ST* ps)
{
assert(ps);
ps->a = (STDataType*)malloc(sizeof(STDataType) * 4);
if (ps->a == NULL)
{
printf("malloc fail\n");
exit(-1);
}
ps->capacity = 4;
ps->top = 0;
}
void StackDestory(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
// 入栈
void StackPush(ST* ps, STDataType x)
{
assert(ps);
//检查是否为满
if (ps->top == ps->capacity)
{
STDataType* tmp = (STDataType*)realloc(ps->a, ps->capacity * 2 * sizeof(STDataType));
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
else
{
ps->a = tmp;
ps->capacity *= 2;
}
}
ps->a[ps->top] = x;
ps->top++;
}
// 出栈
void StackPop(ST* ps)
{
assert(ps);
assert(ps->top > 0);
ps->top--;
}
STDataType StackTop(ST* ps)
{
assert(ps);
assert(ps->top > 0);
return ps->a[ps->top - 1];
}
int StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
队列
队列是一个先进先出的数据结构。
入队列操作
出队列操作
队列代码实现如下:
#pragma once
#include <stdio.h>
#include <stdbool.h>
#include <assert.h>
#include <stdlib.h>
struct BinaryTreeNode;
typedef struct BinaryTreeNode* QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
}Queue;
void QueueInit(Queue* pq);
void QueueDestory(Queue* pq);
// 队尾入
void QueuePush(Queue* pq, QDataType x);
// 队头出
void QueuePop(Queue* pq);
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
int QueueSize(Queue* pq);
bool QueueEmpty(Queue* pq);
#include "Queue.h"
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;
}
void QueueDestory(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
}
// 队尾入
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
if (pq->tail == NULL)
{
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
}
// 队头出
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->head);
if (pq->head->next == NULL)
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
}
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->head);
return pq->head->data;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(pq->head);
return pq->tail->data;
}
int QueueSize(Queue* pq)
{
assert(pq);
int size = 0;
QNode* cur = pq->head;
while (cur)
{
size++;
cur = cur->next;
}
return size;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL;
}