文章目录

  • 1 声明
  • 2 线性表
  • 2.1 线性表的顺序存储结构—顺序表
  • 2.2 线性表的链式存储结构—链表
  • 2.3 比较
  • 2.4 线性表的其他存储方法—静态链表和间接寻址
  • 3 栈和队列
  • 3.1 栈
  • 3.1.1 栈的顺序存储结构—顺序栈
  • 3.1.2 栈的链式存储结构—链栈
  • 3.1.3 比较
  • 3.2 队列
  • 3.2.1 队列的顺序存储结构—循环队列
  • 3.2.2 队列的链式存储结构—链队列
  • 3.2.3 比较
  • 4 字符串和多维数组
  • 4.1 字符串
  • 4.2 多维数组
  • 5 树和二叉树
  • 5.1 树的存储结构
  • 5.2 二叉树的存储结构
  • 6 图
  • 6.1 图的存储结构
  • 6.2 最小生成树
  • 6.3 最短路径
  • 6.4 有向无环图
  • 7 查找技术
  • 7.1 线性表查找技术
  • 7.2 树表查找技术
  • 7.2.1 二叉排序树
  • 7.2.2 平衡二叉树
  • 7.3 散列表查找技术
  • 7.3.1 散列函数的设计
  • 7.3.2 处理冲突的方法
  • 8 排序技术
  • 8.1 插入排序
  • 8.1.1 直接插入排序
  • 8.1.2 希尔排序
  • 8.2 交换排序
  • 8.2.1 起泡排序
  • 8.2.2 快速排序
  • 8.2.2.1 挖坑法快速排序
  • 8.2.2.2 hoare法快速排序
  • 8.3 选择排序
  • 8.3.1 简单选择排序
  • 8.3.2 堆排序
  • 8.4 归并排序
  • 8.5 分配排序
  • 8.5.1 桶排序
  • 8.5.2 基数排序
  • 8.6 排序算法的比较
  • 8.6.1 时间复杂度
  • 8.6.2 稳定性
  • 9 索引技术
  • 9.1 线性索引技术
  • 9.2 树形索引
  • 9.2.1 2-3树
  • 9.2.2 B 树
  • 9.2.3 B+树
  • 10 结语


1 声明

此博客内容总结自 清华大学出版社 数据结构(C++版)ISBN 978-7-302-24416-5,仅用于个人学习,代码均为本人编写。

c pdf下载 数据结构与算法 数据结构c++电子版_c++

2 线性表

定义:是n(n >= 0)个具有相同类型的数据元素的有限序列。

通常用一维数组来实现。

2.1 线性表的顺序存储结构—顺序表

时间复杂度:按位查找O(1);按值查找O(n);插入O(n);删除O(n)

注:插入和删除操作存在插入点(或删除点)之后的元素的平移。

2.2 线性表的链式存储结构—链表

1、单链表

template<class DataType>
struct Node
{
	DataType data;
	Node<DataType> * next;
};

设p为一个指针变量。

指针p指向某个Node类型的节点,该节点用 *p 表示,*p为节点变量,指针p指向的节点简称节点p。

p->data为存放的数据,p->next为存放后继节点地址的指针。e.g. p指向ai,那么p->next指向ai+1。

2、循环链表

把单链表终端节点的指针域由空指针改为指向头节点。

3、双链表

template<class DataType>
struct DulNode
{
	DataType data;
	DulNode<DataType> * prior, * next;
};

(1)、将新节点s插入到节点p后面

s->prior = p;
s->next = p->next;
p->next->prior = s;
p->next = s; //注意顺序

(2)、删除p节点

p->prior->next = p->next;
p->next->prior = p->prior;
free p;

2.3 比较

1、时间性能比较(时间复杂度)

按位置查找,顺序表的O(1)优于链表的O(n)。

插入和删除操作,链表的O(1)优于顺序表的O(n)。

2、空间性能比较
顺序表的存储空间利用率更高,但需要预分配存储空间,可能造成不充分利用。

2.4 线性表的其他存储方法—静态链表和间接寻址

1、静态链表

用数组来模拟单链表。

template<class DataType>
struct SNode
{
	DataType data;
	int next;// 后继元素所在的数组下标
};

2、间接寻址

原来是在数组中存储数据元素,间接寻址方法在数组中存储指向该数据元素的指针。

进行插入和删除操作时,与数组类似,但移动的是指针而不是数据元素,改进了时间性能。

3 栈和队列

3.1 栈

3.1.1 栈的顺序存储结构—顺序栈

1、top

top指向栈顶元素在数组中的位置。

栈空时,top = -1;栈满时,top = stack.size() - 1。

2、两栈共享空间

一个栈的栈底为数组的始端,另一个栈的栈底为数组的末端。

3.1.2 栈的链式存储结构—链栈

单链表的头部作为栈顶。

3.1.3 比较

时间复杂度一样。

空间复杂度:顺序栈要初始化一个固定的长度,链栈需要指针域,各有所长。

3.2 队列

队头出队,从队尾入队。

3.2.1 队列的顺序存储结构—循环队列

1、front和rear

front是队头元素的前一个位置,rear为队尾的位置。

因此,size为5的数组最多放四个元素,有一个为空,为front指向的位置。

2、读取队头元素

return data[(front+1) % QueueSize]

3、入队

rear = (rear+1) % QueueSize;
data[rear] = x;

4、出队

front = (front+1) % QueueSize;
return data[front];

5、判空

return rear == front;//true为空

6、判满

return (rear+1) % QueueSize == front;//true为满

3.2.2 队列的链式存储结构—链队列

入队:链表尾插

出队:头部删除

3.2.3 比较

与栈类似,但不存在两个队列共享一个数组(两个栈可以共享一个数组)。

4 字符串和多维数组

4.1 字符串

1、字符串的比较

在计算机系统中,每个字符都有唯一的数值表示,称为字符编码,ASCII码是最常见的一种。

"abcd" = "abcd";
"abc" < "abcd";
"abac" < "abae";

2、字符串的存储结构

C/C++中用 '/0’来表示串的结束,占一个空间。

3、模式匹配

在主串S中寻找子串T,若成功找到,返回T在S中的位置;失败则返回0。

(1)、BF算法

朴素暴力的算法,最坏时间复杂度O(mn)。

int i = 0, j = 0;
while(S[i] != '/0' && T[j] != '/0'){
	if(S[i] == T[j]){ i++; j++; }
	else{ i = i - j +1; j = 0; }
}
if(T[j] == '/0') return i-j+1;
else return 0;

(2)、KMP算法
相比BF算法,主串i指针无需回溯。匹配失败时,i不动,j = next[j]。并且next只与T有关。

匹配相等的前缀序列中,主串的某个后缀正好是模式的前缀,那么就可以将模式向后滑动到与这些相等字符对齐的位置,并从该位置继续比较。

时间复杂度O(m+n)

4.2 多维数组

1、矩阵的压缩存储—使用多维数组或链表存储矩阵

(1)、对称矩阵的压缩存储

只需存储下三角部分(包括主对角线),因此开辟一个n(n+1)/2大小的数组。

对于元素aij(i>=j),元素在数组中的下标 k = i(i-1)/2 + j-1;

(2)、三角矩阵的压缩存储

与对称矩阵类似,区别在于,例如上三角矩阵,那么除了上三角部分(包括对角线)其余部分都是一个常数,c。

因此开辟一个n(n+1)/2 + 1大小的数组,最后一位储存c。

(3)、对角矩阵的压缩存储

对角矩阵中,所有元素都集中在以主对角线为中心的带状区域,其余元素为0。

存储方式1:与上面两种矩阵类似,存在数组中。

存储方式2:将对角线立起来,存在二维数组中。
例如对角矩阵主对角线为m,带宽为n,则将其存在m * n的数组中。
aij = bts;
t = i; s = j-i+2;

(4)、稀疏矩阵的压缩存储

存储方式1:三元组顺序表

//存储非零元素的位置和值
struct element
{
int row, col;
DataType item;
};

struct SparseMatrix
{
element data[100];
int mu, nu, tu;//行数,列数,非零元个数
}

存储方式2:十字链表

在矩阵非零元素改变的情况下相比三元组顺序表存储更优。

每一个节点由三部分组成:
element(包括row, col, item)
right,指向同一行中的下一个节点
down,指向同一列中的下一个节点

HA可用于对某一行或某一列头指针的快速查找。

c pdf下载 数据结构与算法 数据结构c++电子版_c pdf下载 数据结构与算法_02

5 树和二叉树

5.1 树的存储结构

1、双亲表示法

用一维数组储存树的各个节点。数组中的一个元素对应树中的一个节点,数组元素包括节点的数据信息以及parent的数组下标。

template <class DataType>
struct PNode
{
	DataType data;
	int parent;
};

2、孩子表示法

1、多重链表表示法

用链表实现。每个节点包括一个数据域和多个指针域,指针域的个数等于树的度,指向孩子节点。

2、孩子链表表示法

可以反映兄弟关系,但查找parent比较困难。

struct CTNode
{
int child;
CTNode * next;
};
template <class DatType>
struct CBNode
{
DataType data;
CTNode * firstchild;
};

c pdf下载 数据结构与算法 数据结构c++电子版_c pdf下载 数据结构与算法_03


3、双亲孩子表示法结合双亲表示法和孩子链表表示法。

c pdf下载 数据结构与算法 数据结构c++电子版_数据结构_04


4、孩子兄弟表示法

二叉链表,除了数据域外,两个指针域分别指向第一个孩子和右兄弟。

template <class DataType>
struct TNode
{
DataType data;
TNode<DataType> * firstchild, * rightsib;
};

c pdf下载 数据结构与算法 数据结构c++电子版_数组_05

5.2 二叉树的存储结构

1、顺序存储

将二叉树按完全二叉树的结构存储,会造成很多空间的浪费。

2、二叉链表

template <class DataType>
struct BiNode
{
DataType data;
BiNode<DataType> * lchild, * rchild;
};

重点:
(1). 前中后序遍历递归算法
(2). 层序遍历:利用队列

3、三叉链表

相比二叉链表多了指向parent的指针。

4、线索二叉树

在二叉链表的基础上进行改进,将二叉链表的空指针改为指向前驱或后继的指针。

6 图

6.1 图的存储结构

1、邻接矩阵

也称数组表示法,用二维数组存储边的信息。

2、邻接表

类似树的孩子链表表示法,对于每个顶点,将所有邻接于它的顶点链成一个单链表。

3、十字链表

主要用于存储有向图。

4、邻接多重表

主要用于存储无向图

6.2 最小生成树

1、Prim算法

图中总的顶点为V,已生成的树的顶点为U,关键在于找到连接U和V-U的最短边来扩充生成树。

设连通网中有n个顶点,时间复杂度O(n2)。适合稠密图。

2、Kruskal算法

找最短边,如果属于两个不同的连通分量,则并入生成树。

设联通网中有e条边,时间复杂度O(eloge),适合稀疏网。

6.3 最短路径

1、Dijkstra算法

用于求单源点最短路径的问题。

2、Floyd算法

用于求每一对顶点之间的最短路径问题。

6.4 有向无环图

1、AOV网与拓扑排序

对AOV网进行拓扑排序

2、AOE网与关键路径

带权有向图。

关键路径的长度是最短工期。

7 查找技术

7.1 线性表查找技术

1、顺序查找

时间复杂度O(n)

2、折半查找

要求线性表中的记录必须有序,一般只适用于静态查找。

时间复杂度O(logn)

(1)、非递归算法

int BinSearch1(int r[], int n, int k){
	low = 1, high = n;
	while(low <= high){
		mid = (low + high) / 2;
		if(k < r[mid])
			high = mid - 1;
		else if (k > r[mid])
			low = mid + 1;
		else
			return mid;
	}
	return 0;
}

2、递归算法

int BinSearch2(int r[], int low, int high, int k){
	if(low > high)
		return 0;
	else{
		mid = (low + high) / 2;
		if(k < r[mid])
			return BinSearch2(r, low, mid-1, k);
		else if (k > r[mid])
			return BinSearch2(r, mid+1, high, k);
		else
			return mid;	
	}
}

7.2 树表查找技术

7.2.1 二叉排序树

1、插入算法

逻辑:如果root是空树,那么将s作为根节点插入,如果s->data小于root->data,那么插入到root的左子树,反之插入右子树。

void InsertBST(BiNode<int> * root, BiNode<int> * s){
	if(root == NULL) root = s;
	else if(s->data < root->data) InsertBST(root->left, s);
	else InsertBST(root->right, s);
}

2、构造函数

设所有节点的val存放在数组 r[ ] 中,对于每一个 r[i] ,申请一个数据域为 r[i] 的节点s,令s->left和s->right为空,并不断调用InsertBST。

3、删除节点

(1)、若p是叶子,那么直接删

(2)、若p只有左子树或右子树,那么就把p的左子树或右子树接到p原来的位置即可

(3)、若p的左右子树均不为空。将p右子树中的最小值s来顶替p。

并再分两种情况,第一种特殊情况:p右子树中的最小节点s就是p的右孩子(s一定没有左子树),那么直接把s的右子树接在p的右边。第二种更一般的情况,用s的右子树代替原来s的位置。

// 第三种情况
par = p;
s = p->left;
// 查找p右子树中最小的节点,为s
while(s->left != NULL){
par = s;
s = s->left;
}
p->val = s->val;
// 特殊情况
if(par == p) par->right = s->right; 
// 一般情况
else par->left = s->right;
delete s;

c pdf下载 数据结构与算法 数据结构c++电子版_c++_06

4、查找算法

void SearchBST(BiNode<int> * root, int k){
	if(root == NULL) return NULL;
	else if(root->data == k) return root;
	else if(root->data > k) return SearchBST(root->right, k);
	else return SearchBST(root->left, k);
}

7.2.2 平衡二叉树

7.3 散列表查找技术

散列 = Hash

7.3.1 散列函数的设计

1、直接定址法

H(key) = a * k + b (a, b为常数)

单调均匀不会冲突但不常用,因为这个方法适用于事先知道关键码的分布的情况。

2、除数留余法

3、数字分析法

如果事先知道关键码并且关键码中有若干位分布较为均匀,可以用这几位来作为散列地址。

4、平方取中法

对关键码进行平方,取中间若干位作为散列地址,因为平方之后中间几位分布较为均匀。

5、折叠法

将关键码分为几部分,将这几部分叠加求和,作为散列地址。

7.3.2 处理冲突的方法

1、开放定址法

开放定址法指的是一旦关键码的散列地址产生了冲突,就去寻找下一个空的散列地址。如何寻找?线性探测法(从冲突的下一个位置开始,依次找),随机探测法(位移量是一个随机数列),二次探测法(探测第i次的散列地址Hi’ = (H(key) + d) % m)

由开放定址法处理冲突得到的散列表叫闭散列表。

2、拉链法

将所有散列地址相同的记录储存在一个单链表中。

8 排序技术

8.1 插入排序

8.1.1 直接插入排序

#include <iostream>

void InsertSort(int r[], int n) {
    int i, j;
    for (i = 1; i < n; i++) {
        int pivot = r[i];
        for (j = i - 1; r[j] > pivot; j--) {
            r[j + 1] = r[j];
        }
        r[j + 1] = pivot;
    }
}

int main() {
    int r[8] = { 1,2,78,4,22,3,4,5 };
    int n = sizeof(r) / sizeof(r[0]);
    InsertSort(r, n);
    for (int i = 0; i < n; i++) {
        std::cout << r[i] << ' ';
    }
    std::cin.get();
}

8.1.2 希尔排序

是直接插入排序的升级版。

第一轮,每间隔d分为一组,共分为d组,对每一组进行直接插入排序;第二轮,间隔为d/2,还是对每一组进行直接插入排序;直到d=1。

#include <iostream>

void ShellSort(int r[], int n) {
    int i, j;
    for (int d = n / 2; d >= 1; d = d / 2) {
        for (i = d; i < n; i++) {
            int pivot = r[i];
            for (j = i - d; r[j] > pivot && j > 0; j = j - d) {
                r[j + d] = r[j];
            }
            r[j + d] = pivot;
        }
    }   
}

int main() {
    int r[8] = { 1,2,78,4,22,3,4,5 };
    int n = sizeof(r) / sizeof(r[0]);
    ShellSort(r, n);
    for (int i = 0; i < n; i++) {
        std::cout << r[i] << ' ';
    }
    std::cin.get();
}

8.2 交换排序

8.2.1 起泡排序

每趟排序之后,exchange位置之后的记录一定是有序的,用bound记录

#include <iostream>
#include <algorithm>

void BubbleSort(int r[], int n) {
    int exchange = n-1, bound;
    while (exchange != 0) {
        bound = exchange;
        exchange = 0;
        for (int i = 0; i < bound; i++) {
            if (r[i] > r[i + 1]) {
                std::swap(r[i], r[i + 1]);
                exchange = i;
            }
        }
    }
}

int main() {
    int r[8] = { 1,2,78,4,22,3,4,5 };
    int n = sizeof(r) / sizeof(r[0]);
    BubbleSort(r, n);
    for (int i = 0; i < n; i++) {
        std::cout << r[i] << ' ';
    }
    std::cin.get();
}

8.2.2 快速排序

8.2.2.1 挖坑法快速排序

每趟排序完成之后,i和j都指向pivot最终的位置。

#include <iostream>
#include <algorithm>

void QuickSort(int *r, int left, int right) {
    if (left > right) return;
    int i = left, j = right, pivot = r[left];
    while (i < j) {
        while (i < j && r[j] >= pivot) j--;
        if (i < j) {
            std::swap(r[i], r[j]);
            i++;
        }
        while (i < j && r[i] <= pivot) i++;
        if (i < j) {
            std::swap(r[i], r[j]);
            j--;
        }
    }
    QuickSort(r, left, i - 1);
    QuickSort(r, i + 1, right);
}

int main() {
    int r[8] = { 1,2,78,4,22,3,4,5 };
    int n = sizeof(r) / sizeof(r[0]);
    QuickSort(r, 0, n-1);
    for (int i = 0; i < n; i++) {
        std::cout << r[i] << ' ';
    }
    std::cin.get();
}
8.2.2.2 hoare法快速排序
#include <iostream>
#include <algorithm>

void QuickSort(int *r, int left, int right) {
    if (left > right) return;
    int i = left, j = right, pivot = r[left];
    while (i < j) {
        while (i < j && r[j] >= pivot) j--;
        while (i < j && r[i] <= pivot) i++;
        std::swap(r[i], r[j]);
    }
    std::swap(r[i], r[left]);
    QuickSort(r, left, i - 1);
    QuickSort(r, i + 1, right);
}

int main() {
    int r[8] = { 1,2,78,4,22,3,4,5 };
    int n = sizeof(r) / sizeof(r[0]);
    QuickSort(r, 0, n-1);
    for (int i = 0; i < n; i++) {
        std::cout << r[i] << ' ';
    }
    std::cin.get();
}

8.3 选择排序

8.3.1 简单选择排序

第一轮把最小的数放在r[0],第二轮把第二小的数放在r[1],以此类推。

#include <iostream>
#include <algorithm>

void SelectSort(int *r, int n) {
    for (int i = 0; i < n - 1; i++) {
        int index = i;
        for (int j = i + 1; j < n; j++) {
            if (r[j] < r[index]) index = j;
        }
        std::swap(r[index], r[i]);
    }
}

int main() {
    int r[8] = { 1,2,78,4,22,3,4,5 };
    int n = sizeof(r) / sizeof(r[0]);
    SelectSort(r, n);
    for (int i = 0; i < n; i++) {
        std::cout << r[i] << ' ';
    }
    std::cin.get();
}

8.3.2 堆排序

1、堆的定义

小根堆:每个节点的值都小于等于其左右孩子节点的值。
大根堆:每个节点的值都大于等于其左右孩子节点的值。

2、堆排序

首先初始化一个大顶堆,从最后一个父亲节点开始构造,最后的那些都是叶子节点,不用管。交换堆顶元素与最后一个元素,再重新构造大顶堆。

如何构造大顶堆?首先用dad指向要筛选的元素,son指向要筛选元素的左孩子,再比较要筛选元素的左右孩子,哪个孩子大,son就指向谁。如果r[son]比r[dad]大,那就交换。

#include <iostream>
#include <algorithm>

void Sift(int* r, int start, int end) {
    int dad = start;
    int son = dad * 2 + 1;
    while (son <= end) {
        if (son + 1 <= end && r[son] < r[son + 1]) son++;
        if (r[dad] > r[son]) return;
        else {
            std::swap(r[dad], r[son]);
            dad = son;
            son = dad * 2 + 1;
        }
    }
}
void HeapSort(int *r, int n) {
    for (int i = n / 2 -1; i >= 0; i--) {
        Sift(r, i, n-1);
    }
    for (int i = n-1; i > 0; i--){
        std::swap(r[0], r[i]);
        Sift(r, 0, i-1);
    }
}

int main() {
    int r[8] = { 1,2,78,4,22,3,4,5 };
    int n = sizeof(r) / sizeof(r[0]);
    HeapSort(r, n);
    for (int i = 0; i < n; i++) {
        std::cout << r[i] << ' ';
    }
    std::cin.get();
}

8.4 归并排序

#include <iostream>

int tmp[100];
void MergeSort(int* r, int left, int right) {
    if (left == right) return;
    int mid = (left + right) / 2;
    MergeSort(r, left, mid);
    MergeSort(r, mid + 1, right);
    int i = left;
    int j = mid + 1;

    for (int k = left; k <= right; k++) {
        if((j > right) || (i <= mid && r[i] <= r[j])){
            tmp[k] = r[i];
            i++;
        }
        else {
            tmp[k] = r[j];
            j++;
        }
    }
    for (int k = left; k <= right; k++) {
        r[k] = tmp[k];
    }
}


int main() {
    int r[8] = { 1,2,78,4,22,3,4,5 };
    int n = sizeof(r) / sizeof(r[0]);
    MergeSort(r, 0, n-1);
    for (int i = 0; i < n; i++) {
        std::cout << r[i] << ' ';
    }
    std::cin.get();
}

8.5 分配排序

8.5.1 桶排序

8.5.2 基数排序

c pdf下载 数据结构与算法 数据结构c++电子版_c++_07

#include <iostream>
#include <algorithm>

int GetMaxBit(int* r, int n){
    int max = 0;
    for (int i = 0; i < n; i++) {
        max = std::max(r[i], max);
    }
    int bit = 1;
    while (max / 10 != 0) {
        max = max / 10;
        bit++;
    }
    return bit;
}

void RadixSort(int* r, int n) {
    int bit = GetMaxBit(r, n);
    int base = 1;
    while (bit--) {
        int count[10] = { 0 };
        int start[10] = { 0 };
        int tmp[10] = { 0 };
        for (int i = 0; i < n; i++) {
            int index = r[i] / base % 10;
            count[index]++;
        }
        for (int i = 1; i < 10; i++) {
            start[i] = count[i - 1] + start[i - 1];
        }
        for (int i = 0; i < n; i++) {
            int index = r[i] / base % 10;
            tmp[start[index]++] = r[i];
        }
        for (int i = 0; i < n; i++) {
            r[i] = tmp[i];
        }
        base *= 10;
    }
}


int main() {
    int r[8] = { 1,2,78,4,22,3,4,5 };
    int n = sizeof(r) / sizeof(r[0]);
    RadixSort(r, n);
    for (int i = 0; i < n; i++) {
        std::cout << r[i] << ' ';
    }
    std::cin.get();
}

8.6 排序算法的比较

8.6.1 时间复杂度

最好情况下,直接插入排序和起泡排序最快;

平均情况下,快速排序最快;

最坏情况下,堆排序和归并排序最快。

c pdf下载 数据结构与算法 数据结构c++电子版_数组_08

8.6.2 稳定性

稳定的:直接插入、起泡、归并、基数;

不稳定的:希尔、快排、简单选择、堆排。

9 索引技术

9.1 线性索引技术

9.2 树形索引

9.2.1 2-3树

特点:一个节点包含一个或两个关键码,每个节点有两个或三个孩子,所有叶子都在同一层。

对于树的各种操作都与二叉树类似。

c pdf下载 数据结构与算法 数据结构c++电子版_数据结构_09

9.2.2 B 树

B树是2-3树的推广,2-3树是3阶B树。

9.2.3 B+树

在B树的基础上按关键码的大小次序链起各个终端节点,形成单链表,并设置头指针。

c pdf下载 数据结构与算法 数据结构c++电子版_c pdf下载 数据结构与算法_10

10 结语