总结大学数据结构

我大学数据结构学的是如图的这本书在这里插入图片描述
接下来的文章也是围绕的这本书展开,这篇文章的目的是为了复习总结所学内容,以及一些扩展学习,若有错误请大家在评论区指出,若有表达不清也请指出,感谢大家的指正。

第一章 绪论

“数据结构”是研究如何合理地,高效地处理数据。该章节简要介绍有关数据结构的基本概念和算法分析方法。

1.1 数据结构的研究内容

在这里插入图片描述
数据结构主要研究非数值计算问题,非数值计算问题无法用数学方程建立数学模型。
数据结构是一门研究非数值计算程序设计中的操作对象,以及这些对象之间的关系和操作的学科。
数据结构目前在计算机科学中是一门综合性的专业基础课。数据结构是介于数学,计算机硬件和软件三者之间的一门核心课程。

1.2 基本概念和术语

数据(Data) 是客观事物的符号表示,是所有能输入到计算机中并被计算机程序处理的符号的总称。
数据元素(Data Element) 是数据的基本单位,在计算机中通常作为一个整体进行考虑和处理。也被称为元素,记录等。数据元素用于完成地描述一个对象。
数据项(Data Item) 是组成数据元素的,有独立含义的,不可分割的最小单位。
数据对象(Data Object) 是性质相同的数据元素的集合,是数据的一个子集。


数据结构(Data Structure) 是相互之间存在一种或者多种特定关系的数据元素的集合。数据结构包括逻辑结构和存储结构两个层次。
数据的逻辑结构是从逻辑关系上描述数据,它与数据的存储无关,是独立于计算机的。
在这里插入图片描述

集合结构,树状结构和图结构属于非线性结构。

  1. 集合结构
    数据元素之间除了属于同一集合的关系外,别无其它关系。
  2. 线性结构
    数据元素之间存在一对一的关系。
  3. 树结构
    数据元素之间存在一对多的关系。
  4. 图结构或网状结构
    数据元素之间存在多对多的关系。


存储结构(Memory Construction)
数据的存储结构是数据对象在计算机中的存储,也称物理结构。两种基本的存储结构,顺序存储结构和链式存储结构

存储结构
顺序存储结构
链式存储结构
  • 顺序存储结构
    顺序存储结构是借助元素在存储器中的相对位置来表示数据元素之间的逻辑关系。
  • 链式存储结构
    链式存储结构是用任意的存储单元存储数据元素,附加指针字段表示元素之间的逻辑关系。

数据类型(Data Type) 是一个值的集合和定义在这个值集上的一组操作的总称。
抽象数据类型(Abstract Data Type, ADT) 是由用户定义的,表示应用问题的数学模型,以及定义在这个模型上的一组操作总称。

1.3 算法和算法分析

算法(Algorithm) 是为了解决某类问题而规定的一个有限长的操作序列。
算法的五个重要特征:有穷性,确定性,可行性,输入,输出。
算法的优劣评价的四个方面:正确性,可读性,健壮性,高效性。
衡量算法效率的方法:事后统计法,事前分析评估算法

  • 事后统计法:先将算法实现,测算其时间和空间开销。
  • 事前分析评估算法:计算算法的渐近复杂度来衡量算法的效率。

问题的规模(The Scale Of The Problem) 是算法求解问题输入量的多少,是问题大小的本质表示。
语句频度(Frequency Count) 是一条语句的重复执行次数。
语句一次执行的具体时间与机器软件,硬件环境密切相关。
算法分析不是精确统计算法实际执行所需时间,而是估计算法中语句的执行次数,从而得到算法执行时间的信息。



算法的时间复杂度:算法执行时间的增长率T(n)和f(n)的增长率相同,记作T(n) = O(f(n))

  • 最好时间复杂度:算法在最好情况下的时间复杂度。
  • 最坏时间复杂度:算法在最坏情况下的时间复杂度。
  • 平均时间复杂度:算法在所有可能情况下,按照输入实例以等概率出现时,算法计算量的加权平均值。

分析时间复杂度例题

第一题:
void Foo(int n){
	int i = 0, j = 0;
	int count = 0;
	for(i = 0; i < n; i++){
		for(j = 0; j < n; j++){
			count++;
		}
	}//count++执行n^2次
	for(i = 0; i < n; i++){
		count++;
	}//count++执行n次
	for(i = 0; i < 100; i++){
		count++;
	}//count++执行100次
	printf("%d\n", count);
}

第一题分析:
函数Foo中基本操作count++ 执行次数的函数f(n)表示如下

f(n) = n^2 + n + 100
当n充分大时 f(n) 与 n^2 的数量级相同,所以记作
T(n) = O(n^2)
技巧:在分析时间度时保留最终f(n)中数量级最大的
第二题:
void Foo(int n){
	int i = 0, count = 0;
	for(i = 0; i < 2*n; i++){
		count++;
	}//count++执行2n次
	for(i = 0; i < 100; i++){
		count++;
	}//count++执行100次
	printf("%d\n", count);
}

第二题分析:
函数Foo中基本操作count++ 执行次数的函数f(n)表示如下

f(n) = 2n + 100
当n充分大时 f(n) 与 n 的数量级相同,所以记作
T(n) = O(n)
这里不是T(n) = O(2n)是因为当n充分大时,常数项2对f(n)的影响不大。
第三题:
void Foo(int n, int m){
	int i = 0, count = 0;
	for(int i = 0; i < n; i++){
		count++;
	}//count++执行n次
	for(i = 0; i < m; i++){
		count++;
	}//count++执行m次
	printf("%d\n", count);
}

第三题分析:
函数Foo中基本操作count++ 执行次数的函数f(n)表示如下

f(n) = n + m
当n充分大时 f(n) 与 n + m 的数量级相同,所以记作
T(n) = O(n + m)
这里不是T(n) = O(n) 或者 T(n) = O(m)是因为当n与m充分大时,n与m对f(n)的影响相同。
第四题:
void Foo(int n){
	int i = 0, count = 0;
	for(i = 0; i < 100; i++){
		count++;
	}
	printf("%d\n", count);
}

第四题分析:
函数Foo中基本操作count++ 执行次数的函数f(n)表示如下

f(n) = 100
当n充分大时 f(n) 与 1 的数量级相同,所以记作
T(n) = O(1)
这里不是T(n) = O(100) 是因为当n充分大时,100与1几乎没有差别。因此所有的O(常熟)统一写为O(1)。
第五题:
 const char* strchr(char* string, int c){
	while(*string != '\0'){
		if(*string == c)
			return string;
		string++;
	}
}//在一个字符串中从头到尾查找一个名为c的字符

第五题分析:
函数strchr中很明显何时返回是不清楚的,最好情况下第一次就返回,最坏情况下字符串长度次(n次)后才返回,平均情况下n/2次返回。
在这种变化的情况下,默认分析时看最坏情况
函数strchr中基本操作string++ 在坏情况下,执行次数的函数f(n)表示如下

f(n) = n
所以时间复杂度记作
T(n) = O(n)

第六题:
void BubbleSort(int* arr, int n){
	assert(arr != NULL);
	int i = 0;
	//n个数只需要冒泡n-1次
	for(i = 0; i < n - 1; i++){
		int flag = 1;
		//标记当前状态
		//1表示有序了,0表示任然无序
		//单趟冒泡
		int j = 0;
		for(j = 0; j < n - 1 - i; j++){
			if(arr[j] < arr[j+1]){
				Swap(&arr[j], &arr[j+1]);
				flag = 0;
			}
		}
		if(flag)//若已经有序则退出
			break;
	}
}

第六题分析:
冒泡排序在最坏情况下,即排的数据为逆序,每次单趟排序的次数一次是n-1直到到1为止,那么

f ( n ) = ∑ i = 1 n − 1 i = n × ( n − 1 ) 2 f(n) = \sum_{i=1}^{n-1}i = {n\times(n-1)\over{2}} f(n)=i=1n1i=2n×(n1)

f(n) = (n^2 - n) / 2
所以时间复杂度记作
T(n) = O(n^2)

第七题:
int BinarySearch(int* a, int n, int x){
	assert(a != NULL);
	int left = 0;
	int right = n - 1;
	while(left <= right){
		int mid = left + ((right-left)>>1);
		if(a[mid] > x){
			right = mid - 1;
		}
		else if(a[mid] < x){
			left = mid + 1;
		}
		else{
			return mid;
		}
	}
	return -1;
}

第七题分析:
二分查找,在最坏情况下,即找不到或者查找数在最左边或者最右边。每次查找完后会至少筛选掉一半的数字,
即 2筛选次数-1 ≤ \leq 元素个数 < < < 2筛选次数
f ( n ) = l o g 2 n f(n) = {log_2{n}} f(n)=log2n
所以时间复杂度记作

T(n) = O(logn)

复杂度的表示中默认logn为 log ⁡ 2 n \log_2{n} log2n


第八题:
long Factorial(int n){
	return n < 2 ? n : Factorial(n-1)*n;
}

第八题分析:
这是一个递归算法计算n的阶乘,在函数Factorial中会去递归Factorial(n-1) × \times ×n直到函数到n等于1后结束,会进行n次调用所以

T(n) = O(n)

递归时间复杂度 = = =递归次数递 × \times ×归函数中的次数


编程练习题:消失的数字
该题要求时间复杂度为(n),意思是在执行 常量×n+常量 次操作内完成题目所规定的要求。
我的解题:用异或的特性完成任务,当两个相同数字相异或时结果为0,题目给定的范围是0~n中消失了一个数,所以只需要遍历一遍该数组,将数组内全部数与0~n进行异或操作相即可知道消失的数字是几了,因为只有这个数出现一次,其余数出现两次。

int missingNumber(int* nums, int numsSize){
    int i = 0;
    int miss = 0;
    for(i = 0; i < numsSize; i++)
    {
        miss ^= (i+1);//1~n异或到miss上
        miss ^= nums[i];//数组内数异或到miss上
    }
    return miss;
}

算法的空间复杂度:对一个算法在运行过程中临时占用存储空间大小的量度,记作S(n)=O(f(n))
解决规模为n的问题开辟n个空间那么S(n) = O(n)
申请常量个空间S(n)=O(1)


编程练习题:数组中数字出现的次数
该题要求时间复杂度是O(n),空间复杂度是O(1)。意思是在执行 常量×n+常量 次操作内且申请常量个空间下完成题目所规定的要求。
我的解题:将这两个不同的数字分在不同的两组,将该题拆解成求两次消失的数字。第一步分组:这个不同的数字的二进制位必然存在不同,所以求出这两个数那个二进制数不同即可将其分成两组。而相同的数字的二进制位必然相同所以一定会被分在同一组从而异或操作消除掉。第二步求每组的求消失的数字。

int* singleNumbers(int* nums, int numsSize, int* returnSize){
    int* ret = (int*)malloc(sizeof(int)*2);//开辟2个int空间,S(n) = O(n)
    *returnSize = 2;
    ret[0] = 0; ret[1] = 0;
    //下面申请的临时空间都是常数次所以S(n) = O(n)
//从右到左,找出那个二进制不同
    int bit = 1;//控制取出的二进制位
    int x = 0;//暂存异或后结果
    int i= 0;
    while(1){
        for(i = 0; i < numsSize; i++){
        //通过按位与(&)取出每个二进制位
            int tmp = 0;//存储该位置上的0/1
            tmp = nums[i] & bit;
            x ^= tmp;
        }
        if(x != 0)//为0说明该二进制相同
            break;
        else
            bit <<= 1;//向左移动,查看下一位
    }
    //将符合分组的数异或到一起
    for(i = 0; i < numsSize; i++){
        if((nums[i]&bit) == 0){
            ret[0] ^= nums[i];
        }else{
            ret[1] ^= nums[i];
        }
    }
    return ret;
}

第二章 线性表

2.1 线性表的定义和特点

线性表(Linear List) 是有n(n ≥ \geq 0)个数据特性相同的元素构成的有限序列。
空表(Empty List) 是n为0的线性表,n为线性表长度。
非空的线性表特点

  • 存在唯一的第一个数据元素。
  • 存在唯一的最后一个数据元素。
  • 除第一个数据元素外,每个数据元素只有一个前驱。
  • 除最后一个数据元素外,每个数据元素只有一个后继。

2.2 线性表的类型定义

线性表是一个相当灵活的数据结构,其长度可根据需要增长或缩短,即对线性表的数据元素不仅可以进行访问,而且可以进行插入和删除等操作。
线性表的基本操作:
如下所有基本操作函数均省去返回值与参数,具体情况在实现相应函数时体现,这里只是将函数名与中文含义相对应。

InitList(); 构建一个空的线性表
DestroyList(); 销毁线性表
ClearList(); 将线性表置为空表
ListEmpty(); 判断线性表是否为空表
ListLength(); 线性表元素个数
GetElem(); 线性表某位置的元素
LocateElem(); 第一个相同元素的位置
PriorElem(); 某元素的前一个元素
NextElem(); 某元素的后一个元素
ListInsert(); 插入元素到线性表的某位置
ListDelete(); 删除线性表某位置的元素
TraverseList(); 遍历并访问所有元素

2.3 线性表的顺序表示和实现

线性表的顺序表示是用一组地址连续的存储单元依次存储线性表元素,这种顺序表称为顺序表(Sequential List) 其特点是逻辑上相邻的数据元素,物理次序也是相邻的。


顺序表是一种随机存储的存储结构

随机存储的体现:在C语言中若构建出线性表后,e1的地址为0x1234,那么就可以通过该地址加若干的元素字节大小去直接访问线性表中任意元素,访问无前提条件。
//顺序表的结构体表示
typedef int SeqLDataType;//方便修改顺序表的元素类
struct SeqList {
	SeqLDataType* pBase;//存储空间的基地址
	int length;//顺序表的元素个数
	int capacity;//顺序表的容量
};
typedef struct SeqList SeqList;

顺序表中基本操作的实现

  • 初始化顺序表
    初始化的目标:为顺序表动态分配一个4个元素大小的数组空间,使得pBase指向该空间的基地址
bool InitList(SeqList* pList){
	assert(pList != NULL);
	pList->pBase = (SeqLDataType*)malloc(sizeof(SeqLDataType)*4);
	if (pList->pBase == NULL) {
		printf("Malloc Fail!\n");
		exit(EXIT_FAILURE);
	}//空间不够则结束程序
	pList->capacity = 4;
	pList->length = 0;
	return true;//分配成功返回true
}

动态分配可以更有效的利用系统资源,不需要顺序表时可以通过DestroyList()释放存储空间。

  • 顺序表的销毁
    销毁的目标:销毁顺序表pBase,且容量为-1,长度为-1。
bool DestroyList(SeqList* pList) {
	if (pList == NULL)	return false;

	free(pList->pBase);
	pList->capacity = -1;
	pList->length = -1;
	return true;
}

时间复杂度:T(n) = O(1)

  • 顺序表的取值
    取值的目标:取出顺序表中第pos个元素,放入x中。
bool GetElem(SeqList* pList, int pos, SeqLDataType* pDeposit) {
	if (pList == NULL)	return false;
	if (pDeposit == NULL) return false;
	if (pos < 1 || pos > pList->length)
		return false;//pos位置非法,false返回

	*pDeposit = pList->pBase[pos - 1];
	return true;
}

时间复杂度:T(n) = O(1)

  • 顺序表的查找
    查找的目标:查找顺序表中是否含有data元素,存在返回其下标,不存在返回-1。
int LocateElem(SeqList* pList, SeqLDataType data) {
	if (pList == NULL)	return -1;

	int i = 0;
	for (i = 0; i < pList->length; i++) {
		if (pList->pBase[i] == data)
			return i;//找到后返回下标
	}
	return -1;//未找到返回-1;
}

时间复杂度:T(n) = O(n)

  • 顺序表的插入
    插入的目标:将给定的元素data插入到所有数据的第pos位置,原pos位置后包含pos向后移动,最终顺序表长度加1。
bool ListInsert(SeqList* pList, int pos, SeqLDataType data) {
	if (pList == NULL)	return false;

	if ((pos < 1) || (pos>(pList->length+1)))	return false;//非法pos返回false
	 
	if (pList->length == pList->capacity){//顺序表容量不够,扩容
		SeqLDataType* tmp = (SeqLDataType*)realloc(pList->pBase, 2*(pList->capacity)*sizeof(SeqLDataType));
		if (tmp == NULL) {
			printf("Realloc Fail!\n");
			return false;
		}//系统内存不足返回false
		pList->pBase = tmp;
		pList->capacity *= 2;
	}
	
	int i = 0;
	for (i = pList->length; i > pos - 1; i--) {//向后移动的元素
		pList->pBase[i] = pList->pBase[i - 1];
	}
	pList->pBase[pos - 1] = data;//放入新元素
	pList->length++;
	return true;
}

时间复杂度:T(n) = O(n)

  • 顺序表的删除
    删除的目标:删除表的第pos个元素,pos后的元素向前移动,最终使得顺序表的长度减1。
bool ListDelete(SeqList* pList, int pos) {
	if (pList == NULL)	return false;

	if (pos < 1 || pos > pList->length)
		return false;//非法pos,返回false

	int i = 0;
	for (i = pos; i < pList->length; i++) {//从前往后移动,覆盖掉第pos个元素
		pList->pBase[pos - 1] = pList->pBase[pos];
	}
	pList->length--;
	return true;
}

时间复杂度:T(n) = O(n)

2.4 线性表的链式表示和实现

当顺序表中存在大量元素,插入与删除元素会移动大量元素性能低,而且由于扩容会有大量空间的浪费,于是链式存储结构应运而生。
线性表链式存储结构的特点:用一组任意的存储单元存储线性表的数据元素(逻辑结构上连续,物理结构不一定连续)。为了让逻辑上连续,除了需要保存数据元素外,还需要存储一个表示下一个元素的信息。这两部分信息组成了某个数据元素的表示,称为节点。其中储存信息的域称为数据域,储存相对位置的域称为指针域。指针域中信息称为指针或者
头节点:整个链表的第0个数据元素,数据域内信息无效,指针域引出链表。

单链表

除了头节点外,每个节点数据域存储有效数据,指针域存储下一个节点的地址,从而达到逻辑上成线性结构。

//单链表结构体表示
typedef int SLDataType;
struct SLNode {
	SLDataType data;//数据域
	struct SLNode* pNext;//指针域
};
typedef struct SLNode SLNode;
单链表中基本操作的实现
  • 单链表的初始化
    初始化的目标:将头指针head创建完成,并将数据域初始为0,指针域初始为NULL。
bool InitList(SLNode** ppHead) {
	assert(ppHead != NULL);
	assert(*ppHead != NULL);
	*ppHead = (SLNode*)malloc(sizeof(SLNode) * 1);
	if (*ppHead == NULL) {
		printf("Malloc Fail!\n");
		exit(EXIT_FAILURE);
	}
	(*ppHead)->data = 0;
	(*ppHead)->pNext = NULL;
	return true;
}
  • 单链表的销毁
    销毁的目标:删除所有节点,并使头节点为NULL。
bool ListDestory(SLNode** ppHead) {
	if ((ppHead == NULL) || (*ppHead == NULL)) return false;

	SLNode* pDel = *ppHead;
	while (pDel != NULL) {
		SLNode* pDelNext = pDel->pNext;
		free(pDel);
		pDel = pDelNext;
	}
	*ppHead = NULL;
	return true;
}
  • 单链表的取值
    取值的目标:从单链表中取出第pos个元素的值,放入pDeposit中。
bool GetElem(SLNode* pHead, int pos, SLDataType* pDeposit) {
	if (pHead == NULL) return false;
	if (pos < 1) return false;//pos位置非法返回false

	SLNode* pPos = pHead;
	for (; (pos > 0)&&(pPos != NULL); pos--) //从第一个元素起从前向后扫描链表
		pPos = pPos->pNext;
	
	if (pPos == NULL)
		return false;//越界访问返回false

	*pDeposit = pPos->data;
	return true;
}

时间复杂度:T(n) = O(n)

  • 单链表的查找
    查找的目标:在单链表中查找data,若存在返回其地址,不存在返回NULL。
SLNode* LocateElem(SLNode* pHead, SLDataType data) {
	if (pHead == NULL) return NULL;
	SLNode* pPos = pHead->pNext;
	while ((pPos != NULL) && (pPos->data != data))
		pPos = pPos->pNext;
	
	return pPos;//找到x则返回pos当前位置,找不到pos正好为NULL
}

时间复杂度:T(n) = O(n)

  • 单链表的插入
    插入的目标:将data数据元素的节点,插入到第pos个元素的位置,使原来的第pos个元素在其后面。
bool ListInsert(SLNode* pHead, int pos, SLDataType data) {
	if (pHead == NULL) return false;
	if (pos <= 0) return false;//插入范围非法,返回false

	//创建新节点
	SLNode* pNewNode = (SLNode*)malloc(sizeof(SLNode)*1);
	if (pNewNode == NULL) {
		printf("Malloc Fail!\n");
		return false;
	}
	pNewNode->data = data;
	pNewNode->pNext = NULL;
	//找到插入位置
	SLNode* pNewPrev = pHead;//插入后新节点前的节点
	SLNode* pNewNext = pNewPrev->pNext;//插入后新节点后的节点
	for (; pos > 1; pos--) {
		pNewPrev = pNewPrev->pNext;
		if (pNewPrev == NULL) {
			free(pNewNode);//插入失败,删除节点
			return false;//插入范围非法,越界插入,返回false
		}
		pNewNext = pNewPrev->pNext;
	}
	pNewPrev->pNext = pNewNode;
	pNewNode->pNext = pNewNext;
	return true;
}

时间复杂度:T(n) = O(n)

  • 单链表的删除
    删除的目标:删除第pos个元素。
bool ListDelete(SLNode* pHead, int pos) {
	if (pHead == NULL) return false;
	if (pos <= 0) return false;
	SLNode* pPrev = pHead;//待删除的前一个元素
	SLNode* pDel = pHead->pNext;
	//找到要删除的节点
	for (; (pos > 1) && (pDel != NULL); pos--) {
		pPrev = pPrev->pNext;
		pDel = pDel->pNext;
	}
	if (pDel == NULL)
		return false;//越界删除,返回false

	pPrev->pNext = pDel->pNext;
	free(pDel);
	return true;
}

时间复杂度:T(n) = O(n)

  • 单链表的创建
    创建的目标:手动输入数据元素,使得存在一个管理链表的头指针管理n个节点的链表。
void CreateList_H(SLNode** ppHead, int n) {
	assert(ppHead != NULL);
	assert(*ppHead != NULL);
	InitList(ppHead);
	SLNode* pPrev = *ppHead;//待插入的前一个元素
	int i = 0;
	for (i = 0; i < n; i++) {
		SLNode* tmp = (SLNode*)malloc(sizeof(SLNode) * 1);
		if (tmp == NULL) {
			printf("Malloc Fail!\n");
			exit(EXIT_FAILURE);
		}
		printf("请输入要创建的第%d个值:", i+1);
		scanf("%d", &(tmp->data));
		tmp->pNext = NULL;
		pPrev->pNext = tmp;
		pPrev = pPrev->pNext;
	}
}

时间复杂度:T(n) = O(n)

循环链表

循环链表(Circular Linked List) 是链表的尾节点的指针域指向头节点,使得整个链表形成环。

双向链表

双向链表(Double Linked List) 是在每个节点的指针域包含两个链,所以既可以从某节点访问其后继,也可以访问其前驱。

//双向链表结构体表示
typedef int ListDataType;
struct ListNode {
	ListDataType data;//数据域
	struct ListNode* pNext;//指针域,指向后继
	struct ListNode* pPrev;//指针域,指向前驱
};
typedef struct ListNode ListNode;

带头结点的双向循环链表基本操作实现

  • 链表初始化
    初始化的目标:初始化头节点,使得pprev指向头节点,pnenxt指向头节点。
bool ListInit(ListNode* pHead) {
	if (pHead == NULL)	return false;

	pHead->data = 0;
	pHead->pNext = pHead;
	pHead->pPrev = pHead;
	return true;
}
  • 链表的销毁
    销毁的目标:删除整个链表,使得头节点为NULL。
bool DestroyList(ListNode** ppHead) {
	if ((ppHead == NULL) || (*ppHead == NULL))	return false;

	ListNode* pDel = (*ppHead)->pNext;//待删除的节点
	while (pDel != (*ppHead)) {
		ListNode* pDelNext = pDel->pNext;//记录下一个节点
		free(pDel);
		pDel = pDelNext;
	}
	free(*ppHead);
	*ppHead = NULL;
	return true;
}
  • 链表的插入
    插入的目标:向链表pPos位置插入元素data
bool ListInsert(ListNode* pPos, ListDataType data) {
	if (pPos == NULL)	return false;

	ListNode* pNewNode = (ListNode*)malloc(sizeof(ListNode) * 1);
	if (pNewNode == NULL) {
		printf("Malloc Fail!\n");
		return false;
	}
	pNewNode->data = data;
	ListNode* pNewPrev = pPos->pPrev;
	//链接节点
	pNewPrev->pNext = pNewNode;
	pNewNode->pPrev = pNewPrev;
	pNewNode->pNext = pPos;
	pPos->pPrev = pNewNode;
	return true;
}
  • 链表的删除
    删除的目标:删除链表中pPos位置的元素,若链表为空返回false。
bool ListErase(ListNode* pPos) {
	if (pPos == NULL)	return false;
	if (pPos == pPos->pNext) return false;//头节点返回false
	ListNode* pPosPrev = pPos->pPrev;
	ListNode* pPosNext = pPos->pNext;
	free(pPos);
	pPosPrev->pNext = pPosNext;
	pPosNext->pPrev = pPosPrev;
	return true;
}
  • 链表是否为空
    若为空返回true,反之返回false。
bool ListEmpty(ListNode* pHead) {
	assert(pHead != NULL);
	return pHead->pNext == pHead;
}
  • 链表的取值
    取值的目标:返回链表中第pos位置的地址。
ListNode* GetElem(ListNode* pHead, int pos) {
	if (pHead == NULL)	return NULL;

	ListNode* ret = pHead->pNext;
	for (; (pos > 1) && (ret != pHead); pos--)
		ret = ret->pNext;

	if (ret == pHead)
		return NULL;//查找越界返回NULL
	return ret;
}
  • 链表的首元素插入
bool ListPushFront(ListNode* pHead, ListDataType x) {
	return ListInsert(pHead->pNext, x);
}
  • 链表的结尾元素插入
bool ListPushBack(ListNode* pHead, ListDataType x) {
	return ListInsert(pHead, x);
}

2.5 顺序表和链表的比较

  1. 空间性能的比较

    顺序表的存储空间必须预先分配,元素个数扩充受一定限制,易造成存储空间浪费或空间溢出现象。顺序表的空间利用率高。由于空间分布连续,集中,所以cup高速缓存命中率高。
    链表不需要预先分配空间,随用随分配。链表的空间利用率较低。
  2. 时间性能的比较

    顺序表取元素时,由于是随机存取结构,直接取出,效率高。插入删除时需要挪动元素,效率低。
    链表存取元素时,由于是顺序存取结构,需要遍历链表,效率低。确定位置的插入删除元素不需要挪动元素,效率高。

2.6 链表合并例题

编程练习题:合并有序单链表
题目要求:将两个升序的单链表,合成一个升序的单链表。我的思路:1.先考虑特殊情况,当任意一个链表为空(不能对空指针解引用),直接返回另一个链表即可。2.当都存在元素(一般情况),两个单链表都从第一个元素依次向后比较,取出较小的元素插入新链表的结尾,直到任意单链表为空,接着直接将未处理的链表链接到新链表尾即可。

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
 //对空指针处理   
    if(list1 == NULL) return list2;
    if(list2 == NULL) return list1;

//确认第一个节点
    struct ListNode* pHead;//新链表头元素
    struct ListNode* pEnd;//新链表尾元素
    if(list1->val < list2->val)
        pHead = list1;
    else pHead = list2;
    pEnd = pHead;
//确认后续节点并插入
    while(list1 != NULL && list2 != NULL){
        struct ListNode* pNext;//下一个要连接的节点
        if(list1->val < list2->val){
            pNext = list1;
            list1 = list1->next;//减少未处理元素链表
        }
        else{    
            pNext = list2;
            list2 = list2->next;//减少未处理元素链表
        }
        //增加已处理元素链表
        pEnd->next = pNext;
        pEnd = pEnd->next;
    }
//链接未处理的节点  
    if(list1 == NULL)
        pEnd->next = list2;
    if(list2 == NULL)
        pEnd->next = list1;
    return pHead;
}

编程练习题:合并两个单链表并部分删除
题目要求:找到list1中a到b的片段,删除该片段,用list2取代该片表(将list2镶嵌入a-1到b+1中)。我的思路:此题考查单链表基本操作销毁链表与链接节点。

struct ListNode* mergeInBetween(struct ListNode* list1, int a, int b, struct ListNode* list2){
    struct ListNode* frontNode = list1;//a-1节点
    struct ListNode* backNode = list1;//b+1节点
    struct ListNode* pA = list1;
    struct ListNode* pB = list1;
    int i = 0;
//找到a-1节点
    for(i = 0; i < a - 1; i++)
        frontNode = frontNode->next;
//a节点
    pA = frontNode->next;
    pB = pA;
//找b节点
    for(i = a; i < b; i++)
        pB = pB->next;
//b+1节点
    backNode = pB->next;
//删除a到b节点
    while(pA != pB){
        struct ListNode* next = pA->next;
        free(pA);
        pA = next;
    }
    free(pA);
//链接list1与list2
    frontNode->next = list2;
    //找list2尾
    while(list2->next != NULL)
        list2 = list2->next;
    list2->next = backNode;

    return list1;
}

第三章 栈和队列

栈与队列是操作受限的线性表

3.1 栈与队列的定义和特点

栈(Stack) 是限定仅在表尾进行插入或删除操作的线性表。
栈顶(top) 是表尾。
栈底(bottom) 是表头。
空栈是不含元素的空表。
栈是后进先出(Last In First Out) 的线性表。

队列(queue) 是限定仅在表的一端进行插入在另一端进行删除操作的线性表。
队头(front) 是插入元素端。
队尾(rear) 是删除元素端。
队列 是先进先出(First In First Out)的线性表。

3.2 栈的表示和操作的实现

顺序栈基本操作的实现

//顺序栈结构体表示
typedef int STDataType;

struct SqStack {
	STDataType* base;//栈底指针,base为空表示栈不存在
	STDataType* top;//栈顶指针,空栈时与base相等
	int capacity;//栈的容量
};
typedef struct SqStack Stack;

  • 顺序栈的初始化
    初始化的目标:为顺序栈初始化一段连续的空间。
bool InitStack(Stack** ppStack) {
	if (ppStack == NULL) return false;//管理栈的结构不存在
	*ppStack = (Stack*)malloc(sizeof(Stack)*1);//管理栈的结构开空间
	if (*ppStack == NULL) {
		printf("Malloc Fail!\n");
		return false;
	}
	(*ppStack)->base = (STDataType*)malloc(sizeof(STDataType)*4);//栈开空间
	if ((*ppStack)->base == NULL) {
		printf("Malloc Fail!\n");
		return false;
	}
	(*ppStack)->top = (*ppStack)->base;//初始化为空栈
	(*ppStack)->capacity = 4;
	return true;
}
  • 顺序栈的销毁
    销毁的目标:将栈空间回收且将管理栈结构的指针置为NULL。
bool DestroyStack(Stack** ppStack) {
	if (ppStack == NULL) return false;//对NULL的处理
	if (*ppStack == NULL) return false;//栈不存在
	free((*ppStack)->base);
	free(*ppStack);
	*ppStack = NULL;
	return true;
}
  • 入栈
    入栈的目标:在栈顶插入一个新的元素。
bool Push(Stack* pStack, STDataType data) {
	if (pStack == NULL) return false;//栈不存在
	if (pStack->base == NULL) return false;//栈不存在
	if (pStack->top == pStack->base + pStack->capacity) {//栈满扩容
		STDataType* tmp = NULL;
		tmp = (STDataType*)realloc(pStack->base, sizeof(STDataType) * 2 * (pStack->capacity));
		if (tmp == NULL) {//扩容失败返回false
			printf("Realloc Fail!\n");
			return false;
		}
		pStack->base = tmp;
		pStack->capacity *= 2;
	}
	*(pStack->top) = data;
	pStack->top++;
	return true;
}
  • 判断栈是否为空
    判空的目标:若为空返回true,反之返回false。
bool StackEmpty(Stack* pStack){
	assert(pStack != NULL);//栈不存在
	assert(pStack->base != NULL);//栈不存在
	return pStack->base == pStack->top;
}
  • 出栈
    出栈的目标:将栈顶元素删除,可选将删除元素放入pData中。
bool Pop(Stack* pStack, STDataType* pData) {
	if (pStack == NULL) return false;//栈不存在
	if (pStack->base == NULL) return false;//栈不存在
	if (StackEmpty(pStack)) return false;//空栈

	pStack->top--;
	if (pData != NULL) {
		*pData = *(pStack->top);
	}
	return true;
}
  • 取栈顶元素
    取元素的目标:返回栈顶元素。
STDataType GetTop(Stack* pStack) {
	assert(pStack != NULL);//栈不存在
	assert(pStack->base != NULL);//栈不存在
	assert(!StackEmpty(pStack));//栈不能为空

	return *(pStack->top - 1);
}

链栈基本操作的实现

//链栈的结构体表示
typedef int STDataType;

struct StackNode {
	STDataType data;
	struct StackNode* pNext;//下个元素地址
};
typedef struct StackNode StackNode;
typedef struct StackNode LinkStack;

  • 链栈的初始化
    初始化的目标:构建空栈,将栈顶指针置为NULL。
bool InitStack(LinkStack** ppStack) {
	if (ppStack == NULL) return false;//对NULL的处理
	*ppStack = NULL;//空栈
	return true;
}
  • 入栈
    入栈的目标:将元素插入到栈顶。(链栈由于头部插入所以需要修改栈顶的指针)
bool Push(LinkStack** ppStack, STDataType data) {
	if (ppStack == NULL) return false;//栈不存在
	//开节点空间
	StackNode* top = (StackNode*)malloc(sizeof(StackNode) * 1);
	if (top == NULL) {
		printf("Malloc Fail!\n");
		return false;
	}
	top->data = data;
	top->pNext = *ppStack;
	*ppStack = top;
	return true;
}
  • 出栈
    出栈目标:删除栈顶元素,可选保存到pData中。(链栈由于栈顶元素是链的第一个元素节点所以需要修改栈顶的指针)
bool Pop(LinkStack** ppStack, STDataType* pData) {
	if (ppStack == NULL) return false;//栈不存在
	if (*ppStack == NULL) return false;//空栈

	if (pData != NULL)
		*pData = (*ppStack)->data;
	StackNode* pNext = (*ppStack)->pNext;//记录下一个元素位置
	free(*ppStack);
	*ppStack = pNext;//挪动栈顶节点
	return true;
}
  • 取栈顶元素
    取栈顶元素的目标:返回栈顶元素值。
STDataType GetTop(LinkStack* pStack) {
	assert(pStack != NULL);//空栈

	return pStack->data;//返回栈顶元素
}
  • 链栈的清空
    清空的目标:清空栈内所有元素,将栈顶指针置为NULL。
bool ClearStack(LinkStack** ppStack) {
	if (ppStack == NULL) return false;//栈不存在

	StackNode* pPos = *ppStack;
	while (pPos != NULL) {
		StackNode* pPosNext = pPos->pNext;
		free(pPos);
		pPos = pPosNext;
	}
	*ppStack = NULL;
	return true;
}

3.3 栈与递归

通常递归可以将大型复杂问题的描述和求解变得简洁和清晰
递归:若在一个函数,过程或者数据结构定义的内部直接或者间接出现定义本身的应用。
定义是递归的数据结构是递归的问题的解决是递归的则常常可以利用递归的方法来解决问题。

递归过程与递归工作栈

通常,当一个函数的运行期间调用另一个函数时,在运行被调用函数前,系统需要完成3件事

  1. 将所有的实参,返回地址等信息传递给被调用函数保存。
  2. 为被调用函数的局部变量分配存储区。
  3. 将控制转移到调用函数的入口。

从被调用函数返回调用函数之前,系统也应完成3见工作

  1. 保存被调用函数的计算结果。
  2. 释放被调用函数的数据区。
  3. 依照被调函数保存的返回地址将控制转移到调用的函数。

分析阶乘函数的递归过程

long Fact(long n){
	long temp;
	if(n == 0)
		return 1;
	else
		temp = n*Fact(n-1);//RetLoc2
	return temp;
}

void main(){
	long n;
	n = Fact(4);//RetLoc1
	return 0;
}

利用栈将递归转换为非递归的方法

  1. 设置一个工作栈存放递归工作记录(包含实参,返回地址,局部变量等)。
  2. 进入非递归调用入口将调用程序传来的实在参数和返回地址入栈。
  3. 进入递归调用入口:当不满足递归结束条件时,逐层递归,将实参,返回地址即局部变量入栈,这一过程可用循环语句来实现(模拟递归分解的过程)。
  4. 递归结束条件满足,将到达递归出口的给定常数作为当前的函数值。
  5. 返回处理:当栈不空的情况下,反复退出栈记录,根据记录中的返回地址进行题意规定操作,即逐层计算当前函数的值,直到栈为空为止(模拟递归求值过程)。

通过如上方法改写后结构不够清晰,可读性差需要进一步改进。

3.4 队列的表示和操作的实现

循环队列

循环队列是用顺序表存储数据元素,存有队头下标和队尾下标的结构。

//循环队列结构体表示
typedef int QDataType;

struct CcQueue {
	QDataType* base;//队列空间的起始地址
	int head;//队头元素的下标,head等于tail为空队列
	int tail;//队尾元素的下标+1
};
typedef struct CcQueue Queue;


初始化:动态分配大小为可存X个元素的数组空间。
bool InitQueue(Queue** ppQ) {
	if (ppQ == NULL)	return false;
	(*ppQ)->base = (QDataType*)malloc(sizeof(QDataType)*MAXSIZE);
	if ((*ppQ)->base == NULL){
		printf("Malloc Fail!\n");
		return false;
	}
	(*ppQ)->head = 0;
	(*ppQ)->tail = 0;
	return true;
}

求队列长度:若不存在队列返回-1,存在队列返回队列长度
int QueueLength(Queue* pQ) {
	if (pQ == NULL)
		return -1;
	return ((pQ->tail - pQ->head) + MAXSIZE) % MAXSIZE;
	//可能存在tail<head的情况,注意计算方式
}

判断队列为空:若为空则返回true,若不为空返回false。
bool IsEmpty(Queue* pQ) {
	assert(pQ != NULL);
	return pQ->head == pQ->tail;
}

入队:当队列不存在时报错,当队列满时失败返回,当队列未满时将data入队。
bool Push(Queue* pQ, QDataType data) {
	assert(pQ != NULL);
	if ((pQ->tail + 1) % MAXSIZE == pQ->head)
		return false;//队列满,入队失败

	pQ->base[pQ->tail] = data;
	(pQ->tail)++;
	(pQ->tail) %= MAXSIZE;
	return true;
}

出队:若队列不存在报错,若队列为空则错误返回,若队列不为空则将队头元素保存至pData中将队头元素删除。
bool Pop(Queue* pQ, QDataType* pData) {
	assert(pQ != NULL);
	if (IsEmpty(pQ))
		return false;//空队列返回false

	if(pData != NULL)//若有存储位置则存储
	*pData = pQ->base[pQ->head];//存储队头元素

	pQ->head = (pQ->head + 1) % MAXSIZE;
	return true;
}

取队头元素:若队列不存在或者为空报错,反之返回队头元素值。
QDataType GetHead(Queue* pQ) {
	assert(pQ != NULL);
	assert(IsEmpty(pQ));
	return pQ->base[pQ->head];
}

销毁队列:回收手动开辟的空间。
bool DestoryQueue(Queue** ppQ) {
	if (ppQ == NULL) return false;
	if (*ppQ == NULL) return false;
	free((*ppQ)->base);
	(*ppQ)->base = NULL;
	*ppQ = NULL;
	return true;
}

链队

链队是指采用链式存储结构实现的队列。以头节点与尾节点唯一确认一个队列。

	//节点结构体表示
typedef int QDataType;

struct QueueNode {
	QDataType data;
	struct QueueNode* next;
};
typedef struct QueueNode QueueNode;

	//链队的结构体表示
struct LinkQueue {
	QueueNode* front;
	QueueNode* rear;
};
typedef struct LinkQueue LinkQueue;


初始化:构造一个只有一个头节点的空队列。

QueueNode* BuyNode(QDataType data) {
	QueueNode* ret = (QueueNode*)malloc(sizeof(QueueNode));
	if (ret == NULL) {
		printf("Malloc Fail!\n");
		exit(EXIT_FAILURE);
	}
	ret->data = data;
	ret->next = NULL;
	return ret;
}

bool InitQueue(LinkQueue* pQ) {
	if (pQ == NULL) return false;

	pQ->front = BuyNode(0);
	pQ->rear = pQ->front;
	return true;
}

入队:创建节点,链入队尾。
bool Push(LinkQueue* pQ, QDataType data){
	QueueNode* newnode = BuyNode(data);
	pQ->rear->next = newnode;
	pQ->rear = pQ->rear->next;
	return true;
}

出队:删除第一个有效节点,将其中内容保存入pdata中。
bool Pop(LinkQueue* pQ, QDataType* pdata) {
	//空队返回
	if (pQ->front == pQ->rear) return false;

	QueueNode* del = pQ->front->next;
	pQ->front->next = del->next;
	*pdata = del->data;
	free(del);
	
	//最后元素被删除
	if (pQ->front->next == NULL) pQ->rear = pQ->front;
	return true;
}

取队头元素:取出对头元素。
QDataType GetHead(LinkQueue* pQ) {
	assert(pQ->front != pQ->rear);
	return pQ->front->next->data;
}

3.5 栈与队列的相互实现例题

编程练习题:用队列实现栈
题目要求:利用两个队列实现一个栈。我的思路:本题考查栈与队列的基本操作。队列先进先出,栈后进先出。两个队列,一个队列存储所有元素,另一个队列为空队列,当要出栈时将存储队列内元素出队存入该队列,直到只剩一个元素,该元素就是栈的最后进入的元素,此时该队列成为存储队列,另一个为空队列。

提示:这里除了要导入之前实现的队列,还需要完善队列,1.队列判断空,2.队列的销毁。

bool Empty(LinkQueue* pQ) {
	return pQ->front == pQ->rear;
}

void Destory(LinkQueue* pQ){
	int ret = 0;
	while (!Empty(pQ))
		Pop(pQ, &ret);
	free(pQ->front);
}
typedef struct {
    LinkQueue q1;
    LinkQueue q2;
} MyStack;

MyStack* myStackCreate() {
    MyStack* ret = (MyStack*)malloc(sizeof(MyStack));
    if(ret != NULL) {
        InitQueue(&ret->q1);
        InitQueue(&ret->q2);
        return ret;
    }
    return NULL;
}

void myStackPush(MyStack* obj, int x) {
    if(!Empty(&obj->q1)) 
        Push(&obj->q1, x);
    else
        Push(&obj->q2, x);
}

int myStackPop(MyStack* obj) {
    int ret = 0;
    LinkQueue* saveq = Empty(&obj->q1) ? &obj->q2 : &obj->q1;
    LinkQueue* nullq = Empty(&obj->q1) ? &obj->q1 : &obj->q2;
    //依次出队入队,出栈的元素不入队
    while(!Empty(saveq)){
        Pop(saveq, &ret);
        if(!Empty(saveq))
            Push(nullq, ret);
    }
    return ret;
}

int myStackTop(MyStack* obj) {
    int ret = 0;
    LinkQueue* saveq = Empty(&obj->q1) ? &obj->q2 : &obj->q1;
    LinkQueue* nullq = Empty(&obj->q1) ? &obj->q1 : &obj->q2;
    //依次出队入队,以访问栈顶
    while(!Empty(saveq)){
        Pop(saveq, &ret);
        Push(nullq, ret);
    }
    return ret;
}

bool myStackEmpty(MyStack* obj) {
    return Empty(&obj->q1) && Empty(&obj->q2);
}

void myStackFree(MyStack* obj) {
    //弹出所有元素
    if(!myStackEmpty(obj)){
        while(!myStackEmpty(obj))
            myStackPop(obj);
    }
    //删除队列
    Destory(&obj->q1);
    Destory(&obj->q2);
    //删除栈
    free(obj);
}

编程练习题:用栈实现队列
题目要求:利用两个栈实现一个队列。我的思路:本题考查栈与队列的基本操作。队列先进先出,栈后进先出。两个栈,一个为入队列栈,一个为出队列栈,当出队时将出队列栈中弹出元素,若出队列栈中没有元素,则出队列栈向入队列栈索要,依次将入队列栈中元素全部弹出,进入出队列栈中,完成后再从出队列栈中弹出元素。


第四章 串,数组和广义表


第五章 树和二叉树


第六章 图

Dijkstra寻找最短路径

namespace chyx
{
	//	存储 类型V 的图, 类型W是 边的类型(边的权重)
	//	Direction false为无向图,true为有向图
	template<class V, class W, W MAX_W=INT_MAX, bool Direction = false>
	class Graph
	{
	public:

		Graph(const V* a, size_t n)
		{
			_vertexs.reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = (int)i;
			}
			
			_matrix.resize(n);
			for (size_t i = 0; i < _matrix.size(); i++)
			{
				_matrix[i].resize(n, MAX_W);
			}
		}

		//获取 v在图中的向量下标
		size_t GetVertexIndex(const V& v)
		{	
			auto it = _indexMap.find(v);
			if (it != _indexMap.end())
			{
				return it->second;
			}
			else
			{
				assert(false);
				return -1;
			}
		}


		//为了手动添加边的权测试
		void AddEdge(const V& src, const V& dest, const W& w)
		{
			size_t srci = GetVertexIndex(src);
			size_t desti = GetVertexIndex(dest);

			_matrix[srci][desti] = w;
			if (Direction)
			{
		
				;
			}
			else
			{
				_matrix[desti][srci] = w;
			}
		}

		void Print()
		{
			for (size_t i = 0; i < _vertexs.size(); i++)
			{
				std::cout << "[" << i << "]" << _vertexs[i] << std::endl;
			}
			std::cout << std::endl;

			for (size_t i = 0; i < _matrix.size(); i++)
			{
				for (size_t j = 0; j < _matrix[i].size(); j++)
				{
					if (_matrix[i][j] == MAX_W)
					{
						std::cout << "*" << "  ";
					}
					else 
					{
						std::cout << _matrix[i][j] << "  ";
					}
				}
				std::cout << std::endl;
			}
		}

		//	从 类型V的src节点计算出 到 其他节点的最短距离(贪心)
		//	dist存储得到的最短路径
		//	parentPath存储到该位置的前一个节点下标(为了存储最短路径的走向)
		void Dijkstra(const V& src, std::vector<W>& dist, std::vector<int>& parentPath)
		{
			size_t srcIndex = GetVertexIndex(src);
			size_t allNumber = _vertexs.size();		//所有节点的数量
			dist.resize(allNumber, MAX_W);			//初始化 最短路径 开始为 全MAX_W 表示不可达
			dist[srcIndex] = 0;						//自己到自己 权值为0
			parentPath.resize(allNumber, -1);				//初始化 所有的前一个节点下标为 -1 表示没有节点

			std::vector<bool> vCheck(allNumber, false);		//已经确定最短路径的集合
			vCheck[srcIndex] = true;

			for(size_t i = 0; i < allNumber; i++)
			{
				//	选最短路径的路径 去找下一个最短路径 
				//  下一个节点不能是已经确定最短路径的节点
				//	如果该路径 的权值 小于 最短路径中保存的值就更新
				W curMin = MAX_W;
				int cur = (int)srcIndex;
				for (size_t j = 0; j < allNumber; j++)
				{
					if ( (vCheck[j] == false) && (dist[j] < curMin) )
					{
						cur = (int)j;
						curMin = dist[j];
					}
				}
				vCheck[cur] = true;

				//松弛更新 通过当前最短路径 找下条附近最短
				for (size_t j = 0; j < allNumber; j++)
				{
					//	从cur 节点到下一节点可达 
					//	且 当前 路径权值+cur到下一节点权值 小于 从src到下一节点最短路径权值
					//	就 更新从src到下一节点最短路径权值 和 下一节点的前一节点是谁
					if (
							(_matrix[cur][j] != MAX_W)
							&& ((dist[cur] + _matrix[cur][j]) < dist[j])
						)
					{
						dist[j] = dist[cur] + _matrix[cur][j];
						parentPath[j] = cur;
					}
				}
			}
		}

	private:
		std::vector<V> _vertexs;				//所有的	节点v的集合	
		std::map<V, int> _indexMap;				//直接拿到某个节点v的下标 KV结构
		std::vector<std::vector<W>> _matrix;	//邻接矩阵
	};
}

第七章 查找

7.1 查找的概念

查找表是由一类型的数据元素或者记录构成的集合。
关键字是数据元素或者记录中某个数据项的值,该值可以标识一个数据元素或者记录。若关键字可以唯一的标识一个记录就称为主关键字,可以识别若干条记录的关键字称为次关键字
查找是指根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素或者记录。存在称为查找成功,不存在称为查找不成功。
动态查找表 若查找的同时对表做修改则相应的表称为动态查找表
静态查找表 若查找完成后表不变的表称为静态查找表
平均查找长度(Average Search Length) 是为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值。

A S L = ∑ i = 1 n P i C i ASL=\sum_{i=1}^{n} P_i C_i ASL=i=1nPiCi

Pi是查找表中第i个记录的概率。
Ci是找到表中其关键字与给定值相等的第i个记录时,和给定值已进行比较的关键字个数。即 查询该记录成功所需的查找次数。

7.2 线性表的查找

顺序查找

折半查找

分块查找

7.3 树表的查找

二叉排序树

B-树

AVL树

AVL树是由前苏联数学家Adelson-Velskii和Landis提出的所以以AVL命名。
AVL树是一个平衡二叉树(Balanced Binary Tree 或 Height-Balanced Tree)。
AVL树的特点:
1.左子树与右子树的高度差的绝对值不超过1。
2.左子树和右子树也是AVL树。

利用平衡因子实现AVL树

节点的基本结构:

	template<class K, class V>
	struct AVLTreeNode
	{
		AVLTreeNode* _pLeft;//指向左子树
		AVLTreeNode* _pRight;//指向右子树
		AVLTreeNode* _pParent;//指向父节点
		std::pair<K, V> _kv;//存储的内容
		int _balanceFactor;  // 平衡因子为右子树高度减左子树高度

		//构造函数
		AVLTreeNode(const std::pair<K, V>& kv)
			:_pLeft(nullptr), _pRight(nullptr), _pParent(nullptr), _kv(kv), _balanceFactor(0)
		{}
	};

插入情况分析:
下图中的双旋的平衡因子的情况只是部分特例,平衡因子结果的更新情况参考《平衡因子情况分析》

在这里插入图片描述
双旋的平衡因子情况分析:

在这里插入图片描述

AVLTree插入代码:

bool insert(const std::pair<K, V>& kv)
{
	if (_pRoot == nullptr)//_root 为空 代表不存在树
	{
		_pRoot = new Node(kv);
		return true;
	}

	//_root 不为空  代表存在 树
	Node* pCur = _pRoot;
	Node* pParent = nullptr;
	while (pCur != nullptr)
	{
		if (pCur->_kv.first < kv.first)	//插入信息比当前大,去右数找
		{
			pParent = pCur;
			pCur = pCur->_pRight;
		}
 		else if (pCur->_kv.first > kv.first)	//插入信息比当前信息小,去左数找
		{
			pParent = pCur;
			pCur = pCur->_pLeft ;
		}
		else 
		{
			return false;	//插入信息已存在返回false
		}
	}

	//走到这里,对pCur进行插入
	pCur = new Node(kv);
	if (pCur->_kv.first < pParent->_kv.first)
	{
		pParent->_pLeft = pCur;
		pCur->_pParent = pParent;
	}
	else if(pCur->_kv.first > pParent->_kv.first)
	{
		pParent->_pRight = pCur;
		pCur->_pParent = pParent;
	}
	else
	{
		printf("插入冗余错误!\n");	//当是非冗余版本时不可能相等,直接assert掉
		assert(false);
	}
	
			//走到这里,对新节点的影响进行更新
			//1.更新平衡因子
			//2.判断是否继续更新,判断是否更新后合理
			
	while (pParent != nullptr)
	{
		//对当前的pCur 与 pParent 进行更新 
		if (pCur == pParent->_pLeft)
		{
			pParent->_balanceFactor--;
		}
		else if (pCur == pParent->_pRight)
		{
			pParent->_balanceFactor++;
		}
		
		//判断更新的合理性 与 更新后是否继续更新
		if (pParent->_balanceFactor == 0)
		{
			//结束更新 
			break;
		}
		else if ((pParent->_balanceFactor == -1) || (pParent->_balanceFactor == 1))
		{
			//继续往上更新
			pCur = pParent;
			pParent = pParent->_pParent;
		}
		else if ((pParent->_balanceFactor == -2) || (pParent->_balanceFactor == 2))
		{
			//旋转处理
			if ((pParent->_balanceFactor == -2) && (pCur->_balanceFactor == -1))//右单旋
			{
				R_R(pParent);
			}

			if ((pParent->_balanceFactor == 2) && (pCur->_balanceFactor == 1))//左单旋
			{
				R_L(pParent);
			}
			
			if ((pParent->_balanceFactor == -2) && (pCur->_balanceFactor == 1))//左右双旋
			{
				Node* parent = pParent;
				Node* left = pParent->_pLeft;
				Node* right = left->_pRight;

				int rightBF = right->_balanceFactor;

				R_L(pCur);
				R_R(pParent);

				//这里之所以分为三种情况更新全部写出,因为希望双旋平衡因子更新与单旋的平衡因子更新低耦合
				if (rightBF == 0) 
				{
					parent->_balanceFactor = 0;
					left->_balanceFactor = 0;
					right->_balanceFactor = 0;
				}
				else if (rightBF == -1)
				{
					parent->_balanceFactor = 1;
					left->_balanceFactor = 0;
					right->_balanceFactor = 0;
				}
				else if (rightBF == 1)
				{
					parent->_balanceFactor = 0;
					left->_balanceFactor = -1;
					right->_balanceFactor = 0;
				}
				else
				{
					std::cout << "R_LR ERROR!" << std::endl;
					assert(false);//不可能出现这种情况,断死
				}


			}

			if ((pParent->_balanceFactor == 2) && (pCur->_balanceFactor == -1))//右左双旋
			{
				Node* parent = pParent;
				Node* right = parent->_pRight;
				Node* left = right->_pLeft;

				int leftBF = left->_balanceFactor;

				R_R(pCur);
				R_L(pParent);
					
				if (leftBF == 0)
				{
					parent->_balanceFactor = 0;
					left->_balanceFactor = 0;
					right->_balanceFactor = 0;
				}
				else if (leftBF == -1)
				{
					parent->_balanceFactor = 0;
					left->_balanceFactor = 0;
					right->_balanceFactor = 1;
				}
				else if (leftBF == 1)
				{
					parent->_balanceFactor = -1;
					left->_balanceFactor = 0;
					right->_balanceFactor = 0;
				}
				else
				{
					std::cout << "R_RL ERROR!" << std::endl;
					assert(false);//不可能出现这种情况,断死
				}
			}

			break;
		}
		else
		{
			//在处理前,AVLTree已经出现关于平衡因子的结构问题
			assert(false);
		}
	}
	return true;
}

补充:红黑树

红黑树是通过节点的颜色来控制二叉搜索树的高度。
红黑树的规则如下:
1.根节点是黑色。
2.不能有连续的红色节点。
3.根节点到每个nullptr节点的黑色节点数一样多。

7.4 散列表的查找


第八章 排序

章节目标:了解几种典型的排序方法,了解算法所依据的原则。

排序(Sorting) 是按照关键字的非递减或者非递增的顺序对一组记录重新进行排序。
排序的稳定性是指在一组记录中存在两个及以上的相同记录时,若每次排序后,这些一样的数据相对位置不变则是稳定的,反之为不稳定。
内部排序是待排序的记录全部存放在计算机内存中进行排序的过程。
外部排序是待排序记录所占空间大,内存不足以一次存储全部记录,需借助外存的排序过程。
内部排序的过程是一个逐步扩大记录的有序序列长度的过程,排序过程中可分为有序序列区和无序序列区。使有序区中的数目增加一个或者几个的操作称为一趟排序。根据各排序过程的不同,内部排序大致分为:插入,交换,选择,归并,分配排序等。
评判排序算法效率的优一般:执行时间,辅助空间。

插入排序

直接插入排序(Straight Insertion Sort)

直接插入排序:将待排序的数据插入到一个有序的数据中,在有序的数据中从头到尾遍历找到待排序的正确位置。

在这里插入图片描述

折半插入排序

折半插入排序:将待排序的数据插入到一个有序的数据中,在有序的数据中通过折半查找找到待排序数据的正确位置。

希尔排序

希尔排序:分组插入,让无序的数据更快的到达待排序数据正确位置附近,将无序数据变为基本有序,在基本有序下直接插入排序。

交换排序

冒泡排序

冒泡排序:在无序数据中,通过遍历交换每次使得最大或者最小数据“浮出水面”。

快速排序

快速排序:在无数数据中,选key,通过比较交换使key放在正确的位置,即形成key左边与右边分别大于与小于key,左右依次选key完成对key的排序。

选择排序

简单选择排序

简单选择排序:在无序数据中,每次遍历比较出最大或者最小的数据放在无序数据的最前面或者后面,缩小无序数据范围直到全部有序。

堆排序

堆排序:通过大堆或者小堆的特点,每次将根节点数据(最大或者最小数据)与最后数据交换,将堆向下调整为大堆或者小堆进一步筛选直到所有数据有序。

归并排序

基数排序


待续…

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

E.d cheng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值