本学期数据结构知识汇总(代码用C语言实现)

文章内容目前还没有做完,之后会按照知识准备,线性表,树,堆这个顺序补充章节(相关的算法内容目前不作补充,先完善数据结构部分)。

重点内容我会用带括号的标题演示,加粗字体则需要记忆,代码只给出一些关键代码,具体是怎么串联的还要靠自己去学。

这种笔记类的文章看个乐呵就差不多了,本来是不打算发出来的,因为里面的内容和课堂上的内容完全没法比,可能只占了70%甚至更低。但有的同学苦于找不到比较适合复习资料,并且也实在是读不下几百上千页的ppt,那咱也只能把这个残次品发出来给各位评鉴一下了。咱学计算机的最不缺的就是开源精神,如果这文章能帮到你的话那就再好不过了

1基础知识的准备

这里还是放上老师的原话:背书不能高分,理想成绩还是要靠花时间看书多实践。

1.1参考资料

除了本学期大家使用的黑皮书以外,算法四、算法导论、计算机程序设计艺术等书目也常常被提起这些都是很经典的算法书,此外,具体数学也是在后续学习中不可或缺的一本重要书籍。其次是博客网站上别人加工好的文章,这类资料的知识覆盖没有书籍全面,但是具有很强的针对性,且都符合人类的阅读习惯(不需要像视频那样拖动进度条)最后就是通过看视频学习了,由于我没有什么好推荐的就先跳过它了。

1.2什么是数据结构,啥又是算法

1.2.1数据结构

数据结构就是对特定问题设计好的代码工具,能帮助我们减少重复编码的实践,并且可以灵活组合去解决更多的计算机领域的问题。“为了合理地使用一台计算机,我们需要理解存在于数据内的结构关系,以及在一台计算机内表示和操作这样的结构的基本技术”。

我们在学习数据结构时需要完成增删查改这些基本功能(当然,课程本身也是要求了一些额外功能的)。

1.2.2算法

算法是用来解决某一类问题的一套解决策略,具有很强的规律性。算法具有有穷性,确定性以及有效性。

1.2.3复杂度分析

顾名思义,复杂度是用来表示一个程序的复杂程度的,我们以此评定一个算法的性能是否足够优良(复杂度是评价算法性能的重要指标)。

1.2.3.1时间复杂度

用于描述程序运行时间的函数之间建立的一种相对的级别,本质是描述相对增长率的不等式。

1.2.3.1.1时间复杂度的推算

在这里我们介绍一个常用方法(循环类算法):写出执行次数,追踪变量变化,找出两者之间的联系并用数学表达式表达;然后确定循环终止条件;联立两个等式求解估算时间复杂度。

(重点:估算时间复杂度的通用方法)

1. 明确输入规模 n

  • 定义 n 的具体含义(如:数组长度、图的顶点数、数值的位数等)。

  • 示例

    • 排序算法:n = 待排序元素个数。

    • 快速幂算法:n = 指数的值(注意不是位数)。

2. 定位核心操作

  • 找到算法中执行次数最多的语句(通常是循环或递归调用)。

  • 示例

    • 循环中的基本操作(如比较、赋值)。

    • 递归函数中的自我调用。

3. 建立执行次数 T(n) 的数学模型

  • 循环结构:分析循环变量的变化规律。

  • 递归结构:写出递推关系式(如 T(n) = aT(n/b) + f(n))。

  • 嵌套操作:乘法法则(嵌套循环)或加法法则(并列操作)。

4. 求解 T(n) 并化简

  • 解方程(如求和、递推式展开、主定理)。

  • 保留最高阶项,忽略低阶项和常数系数。

经典问题:最大子序列和  leecode53    

2线性表(Linear List)

线性表,顾名思义:线性表 是数据结构中最基本、最常用的一种结构,用于存储一组具有线性关系的数据元素。它的特点是数据元素之间按顺序排列,每个元素最多只有一个前驱和一个后继(除首尾元素外)。

线性表主要包括数组(Array/List),链表(LinkedList)栈(Stack)以及队列(Queue)

C语言里我们最常见最常用的就是数组,就先从他开始讨论吧。

2.1数组

回忆数组的特点:数组的查询是非常方便的,只要我们知道目标的索引(index)是多少就能快速定位获取(因为数组的内存分配是紧密连续的)。 缺点也很明显,因为要确保最后的内存是连续的,所以大小在声明之后就不能再改变了,则有可能引起空间的不足或者是浪费。

除此之外还有二维数组或者其他的高维数组,这里,我们着重了解它们的存储方式就好了(本质上还是紧密连续的内存空间),还有锯齿数组这些扩展概念这里就不多做介绍了,反正以后会用到。

2.1.1动态数组/向量(Vector)的实现

为了弥补缺点,人们就设计出了动态数组,也被称为向量。向量能够重新分配自己的内存空间大小,那我们要怎么实现呢?

为了对数组实现动态的内存分配,我们需要用指针来操作;然后就是为了区分数组的实际长度以及它的容量,在结构体中应该要使用两个变量,也就是len和capacity。此外加上它自己需要存储的数据data,结构体就应该是:

struct ArrayList{
    ElementType *data;
    int len;
    int capacity;
};

然后来考虑基础功能的实现(具体操作和数组一致,这里就直接放代码了)

当然,需要注意的是,我们在删除以及增加元素时,会导致数组中出现“空位”(尾部修改除外),在这种情况发生时,我们要对数据的索引进行调整(详见removeByIndex以及addItem)

List createList() {
    List L = (PtrArrayList)malloc(sizeof(struct ArrayList));
    if (L == NULL) {
        printf("Out of memery, create List fail\n");
        return NULL;
    }

    L->data = malloc(sizeof(ElementType) * DEFAULT_CAPACITY);
    if (L->data == NULL) {
        printf("Out of memery, create list array fail\n");
        free(L);
        return NULL;
    }
    memset(L->data, 0, DEFAULT_CAPACITY);

    L->capacity = DEFAULT_CAPACITY;
    L->len = 0;

    return L;
}


int addItem(List list, ElementType item, int pos) {
    int i;
    if (list == NULL) {
        printf("\nlist is null\n");
        return ERROR;
    }
    else if (list->len == list->capacity) {
        printf("\nlist is full!\n");
        return ERROR;
    }
    else if (pos < 0 || pos >= list->capacity) {
        printf("\npos out of range!\n");
        return ERROR;
    }

    for (i = list->len - 1; i >= pos; i--) {
        list->data[i + 1] = list->data[i];
    }
    list->data[pos] = item;
    list->len++;

    return OK;
}

int addItemTail(List list, ElementType item) {
    return addItem(list, item, list->len);
}

int findItem(const List list, ElementType item) {
    int i = 0;
    if (list == NULL)
        return ERROR;

    //	for(i = 0; i < list->len; i++){
    //		if(list->data[i] == item)
    //			return i;
    //	}
    //	return ERROR;
    while (i < list->len && list->data[i] != item)
        i++;
    return i > (list->len - 1) ? ERROR : i;
}

int removeItem(List list, ElementType item) {
    int i;
    if (list == NULL) {
        printf("\nlist is null\n");
        return ERROR;
    }

    int pos = findItem(list, item);
    if (pos != ERROR) {
        for (i = pos; i < list->len - 1; i++)
            list->data[i] = list->data[i + 1];
        list->data[list->len - 1] = 0;
        list->len--;
        return pos;
    }

    return ERROR;
}
int removeByIndex(List list, int pos) {
    int i;
    if (list == NULL) {
        printf("\nlist is null\n");
        return ERROR;
    }
    else if (pos < 0 || pos >= list->len) {
        printf("\npos out of range!\n");
        return ERROR;
    }

    for (i = pos; i < list->len - 1; i++)
        list->data[i] = list->data[i + 1];
    list->data[list->len - 1] = 0;
    list->len--;
    return OK;
}

int setItem(List list, ElementType item, int pos) {
    if (list == NULL) {
        printf("\nlist is null\n");
        return ERROR;
    }
    else if (pos < 0 || pos >= list->len) {
        printf("\npos out of range!\n");
        return ERROR;
    }

    list->data[pos] = item;
    return OK;
}

ElementType getItem(List list, int pos) {
    if (list == NULL) {
        printf("\nlist is null\n");
        return ERROR;
    }
    else if (pos < 0 || pos >= list->len) {
        printf("\npos out of range!\n");
        return ERROR;
    }

    return list->data[pos];
}

int isEmpty(List list) {
    if (list == NULL)
        return ERROR;
    return (list->len == 0);
}

void printList(const List list) {
    int i;
    if (list != NULL) {
        printf("\n[ ");
        for (i = 0; i < list->len; i++)
            printf("%d ", list->data[i]);
        printf("]\n");
    }
}

(重点:动态数组的resize)

之后就是考虑扩容与缩容(resize)的问题了,我们把数组视为一个集装箱(数据结构),里面呢又有很多紧密排序的盒子(按索引排列的内存空间),盒子当中又装有很多货物(元素)。思路也很简单,为了扩容或者缩容,我们直接拿来一个全新的,满足容量需求的集装箱把货物搬过去就好了。因此,动态数组resize的实质是:对数组内存的再分配

另一个办法就是直接使用自带的realloc重新分配内存空间(这里就不补充对应的知识了,这属于C语言基础)。

2.1.2动态数组的性能讨论

之前也说过了,数组的查找是非常方便的,通过代码实践我们也发现,数组在除尾部以外的地方插入删除元素是非常麻烦的,需要对修改位置之后的所有元素都进行挪动。时间复杂度达到O(N)

预告:枚举(模拟、二进制枚举、二分枚举) 

           双指针(数组反转、数组循环移动、滑动窗口、快慢指针)

2.2链表(LinkedList)

简介:链表相对于数组而言是一种松散而非连续(逻辑有关,物理无关)的数据结构,具体表现于链表由一个又一个节点(Node)组成,节点又散落在内存各处。

上一节我们已经讨论过了数组的内存布局、访问方式以及性能特点,不难发现数组在多增删的场景下的性能是比较低下的,为了弥补这一缺点,计算机科学家又创造出来了链表这个数据结构。

一个表总结差异:

(重点:链表 vs. 数组的核心区别)

特性链表数组(动态/静态)
内存布局非连续,动态分配连续内存
访问方式顺序访问(O(n)随机访问(O(1)
插入/删除O(1)(已知位置)O(n)(需移动元素)
扩容方式动态增删节点,无内存拷贝realloc 可能触发内存拷贝
缓存友好性差(内存不连续,易缓存未命中)优(连续内存,缓存命中率高)
内存开销每个节点额外存储指针仅存储数据

2.2.1链表的实现

在此之前我们再来进行一些讨论,虚拟头节点(也作哑元Dommy),这个结构能帮助我们获取操作链表的对象,并且在一些操作场合中是非常友好的。Head和Tail能够确保每个节点的前驱后继都不为空。

链表的节点:包括存储的数据,以及指向后继的指针

struct Node{
    ElementType Element;
    Position Next;
    //如果是双链表则还有
    //Position Prev;
};

链表的基本功能(自己填空)

List createList() {
    Position L = (Position)______(sizeof(struct Node));
    if (!L) {
        printf("\n ERROR");
    }
    L->Next = ______;
    return L;
}
int IsEmpty(List L) {
    return ______ = NULL;  // 注意:这里有错误需要修正
}
Position Find(ElementType X, List L) {
    Position P = ______;
    while (______ && ______) {
        P = ______;
    }
    ______  // 缺少返回语句
}
void Insert(ElementType X, List L, Position P) {
    Position TmpCell = (Position)______(sizeof(struct Node));
    if (L == NULL)
        printf("\ninsert list failed, out of memery!\n");

    TmpCell->Element = ______;
    TmpCell->Next = ______;
    ______ = TmpCell;
}
void Delete(ElementType X, List L) {
    Position Prev, TmpCell;
    Prev = ______(X, L);
    if (Prev != NULL) {
        TmpCell = ______;
        Prev->Next = ______;
        ______(TmpCell);
    }
}
Position Last(List L) {
    if (______(L))
        return NULL;

    Position P = ______;
    while (______ && ______)
        P = ______;
    return ______;
}
void PrintList(List L) {
    Position P = ______;
    printf("\nDummyHead->");
    while (______) {
        printf("[%d]->", ______);
        P = ______;
    }
    printf("NULL\n");
}

(代码不能给完整的,不然直接照抄就完事了)

之后是一些和链表有关的经典问题

预告:链表反转、链表中的环(快慢指针解决)、约瑟夫环、多项式、计算器、链表归并

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值