单链表
单链表的类定义(三种方法)
- 复合类
class List;//List类前视声明
class LinkNode {
friend class List;//声明List类为友元类
private:
int data;
LinkNode *link;
};
class List {
public:
//公操作部分
private:
LinkNode *first;
};
- 嵌套类
class List {
public:
//公操作部分
private:
class LinkNode {//嵌套的类
public:
int data;
LinkNode *link;
};
LinkNode *first;//单链表的头指针
};
- 基类和派生类
class LinkNode {
protected:
int data;
LinkNode *link;
};
class Link :public class LinkNode {
private:
LinkNode *first;
public:
//链表相关操作
};
- 用struct定义LinkNode类
struct LinkNode{
int data;
LinkNode *link;
};
class Link {
private:
LinkNode *first;
public:
//链表相关操作
};
- 单链表的插入
bool List::Insert(int i, int& x) {
//将新元素x插入到第i个元素之后。i从1开始,若i=0表示插入到第一个结点之前
if (first == NULL || i == 0) {//该情况为第一种情况,插入的为空表或是第一个结点之前
LinkNode *newNode = new LinkNode(x);//新建一个结点
if (newNode == NULL) { cerr << "存储空间分配错误!\n"; exit(1); }
newNode->link = first; first = newNode;//新结点插入first后面,即第一个结点之前
}
else {//插入中间结点或是最后一个结点情况一样
LinkNode *current = first;//需要一个临时指针,用来指向我们需要插入的位置
for (int k = 1; k < i; k++)//需要找到需要插入的位置
if (current == NULL)break;
else current = current->link;
if (current == NULL) { cerr << "插入位置无效\n"; return false; }
else {
LinkNode *newNode = new LinkNode(x);
if (newNode == NULL) { cerr << "存储空间分配错误!\n"; exit(1); }
newNode->link = current->link;
current->link = newNode;
}
}
return true;
}
单链表的删除需要新建指针(del)来删除我们想删除的结点,del是为了删除,如果是第一个结点:del = first; first = first->link; delete del;
如果为中部或尾部删除,则:
del = current->link; current->link = del->link; delete del;
而如果是带有头结点的单链表,则插入和删除都只有一个情况,且与上面的后面一种插入和删除操作一致。
前插法建立单链表即为每次插入都是插入头结点后面,后插法建立单链表需要多一个尾指针last,每次插入都在last指针后面插入,last一开始在附加头结点处。
循环链表在单链表的基础上将尾与头链接,形成一个循环。在循环链表中进行插入操作,如果是插入于表头位置,则在链尾处进行修改,一般链尾指针记为rear。
双向链表的结点结构不同,多加了前驱指针。而双向链表一般带有头结点,头结点的前驱指针指向尾结点,头结点的后继指针指向第一个结点。
双向链表的定位运算要区分是哪一个方向上的方向,那么形参需要一个int变量表示方向:Locate(int i,int d)//若d=0则表示在前驱方向需要第i个结点
不管是搜索、插入还是删除都需要分d=0或d≠0来进行操作。双向链表的插入需要改变四个指针,根据d的值区分前插还是后插,但是需要改变的指针为新结点的lLink和rLink、插入位置前一结点的rLink和插入位置后一结点的lLink(前序插入)。
单链表的逆序
非递归:
node* reverseList(node* H)
{
if (H == NULL || H->next == NULL) 链表为空或者仅1个数直接返回
return H;
node* p = H, *newH = NULL;
while (p != NULL) 一直迭代到链尾
{
node* tmp = p->next; 暂存p下一个地址,防止变化指针指向后找不到后续的数
p->next = newH; p->next指向前一个空间
newH = p; 新链表的头移动到p,扩长一步链表
p = tmp; p指向原始链表p指向的下一个空间
}
return newH;
}
栈为后进先出 ( L I F O (LIFO (LIFO, L a s t Last Last I n In In F i r s t First First O u t Out Out),栈可定义为只允许在表末端进行插入和删除的线性表,可以插入删除的为栈顶,不允许插入删除的为栈底。插入和删除之前分别看是否栈满和栈空。对栈中的操作有Pop(弹栈)还有getTop(返回栈顶元素的地址),而这两者的区别在于前者改变了栈顶指针的值,后者没有改变栈顶指针的值。
顺序栈
顺序栈需要有一个栈数组,栈顶指针和该栈的最大可容纳元素个数。初始化时,栈顶指针top初始化为-1,入栈时,top+1指向入栈的位置,如果top为maxSizx-1,则栈满,需要进行栈的溢出处理。出栈时,top-1,如果top==-1则栈空。
链式栈
链式栈的结点与单链表的结点结构一样,可以直接使用单链表的结点struct定义。
链式栈的清空、插入和删除函数:
template<class T>
LinkedStack<T>::makeEmpty() {
LinkNode<T> *p;
while (top) {
p = top; top = top->link; delete p;
}
}
template<class T>
LinkedStack<T>::Push(const T& x) {
top = new LinkNode<T>(x, top);
assert(top != NULL);
}
template<class T>
LinkedStack<T>::Pop(T& x) {
if (IsEmpty() == true)return false;
LinkNode<T> *p = top;
top = top->link;
x = p->data; delete p;
return true;
}
递归解决汉诺塔问题
void Hanoi(int n, String A, String B, String C) {
if (n == 1)cout << "将A移至C";
else {
Hanoi(n - 1, A, C, B);
cout << "将A移至C";
Hanoi(n - 1, B, A, C);
}
}
队列为先进先出
(
F
I
F
O
(FIFO
(FIFO,
F
i
r
s
t
First
First
I
n
In
In
F
i
r
s
t
First
First
O
u
t
Out
Out),允许插入的一端为队尾(rear),允许删除的一端为队头(front),插入时将新元素添加到rear所指位置,然后rear+1(所以rear其实指向的是最后一个结点的后面一个空位置)。如果删除头结点,那么front+1。当front=rear则队空,当rear==maxSize则队满。但是可能front前面还有空位置,形成“假溢出”,这时候就需要我们的循环队列。循环队列实现时,为
front = (front + 1) % maxSize; rear = (rear + 1) % maxSize;
为了判断队满和队空,将队满的情况为当rear指针指向front的前一个位置就认为已满。
前面的队列和循环队列都是用的类似于数组的结构,所以内存有限,那么引入链式队列。链式队列可以直接继承单链表的结构,链式队列有队头指针和队尾指针。队头指针指向第一个结点,队尾指针指向最后一个结点。链式队列在插入时,如果队列为空,那么需要new一个新结点,它既是队头也是队尾。
优先级队列其实就是一个数组值从小到大的数组。
双端队列是两端都可以插入和删除,所以关于队头和队尾的函数都有三个,分别是读队头(队尾)函数、队头(队尾)插入函数和队头(队尾)删除函数。
双端队列(Deque)如果是数组表示法(SeqDeque),那么继承抽象基类(Deque)和循环队列类(SeqQuque)。因为循环队列是用数组的,故双端队列的数组表示法是继承于循环队列。而循环队列只有队尾插入和队头删除,故双端队列里需要添加队尾删除和队头插入的方法。
双端队列用链表表示,那么是继承于抽象基类(Deque)和链式队列。那么这里不是循环队列,所以只有队列的头结点front,删除队列尾结点时,需要从头找到尾,找到rear结点的前一个结点,再进行删除。
稀疏矩阵
如果一个矩阵零元素远远大于非零元素,那么可以说是稀疏矩阵。我们需要进行压缩存储,那么我们用一个三元组数组唯一地存储稀疏矩阵里的非零元素。三元组数组包含了这个非零元素的行号、列号还有其值,一般添加进数组的顺序为一行一行地添加。对稀疏矩阵的操作通常为转置、求和还有两矩阵的乘积。
稀疏矩阵快速逆置算法
广义表
广义表的定义为递归的,因为在表中有表。广义表
树
树的结点有三个部分,分别是data域、指向右子树的rightChild指针和指向左子树的leftChild指针。这种是二叉链表,也有三叉链表,三叉链表多了一个指向父结点的指针,便于我们找到任一结点的父结点。树中需要一个表头指针指向根结点,作为该树的访问点。二叉链表和三叉链表都可以是静态链表结构,即把链表放在一维数组中。
template<class T>
struct BinTreeNode
{
T data;
BinTreeNode<T> *rightChild,*leftChild;
BinTreeNode():leftChild(NULL),rightChild(NULL){}
BinTreeNode(T x,BinTreeNode<T> *l=NULL, BinTreeNode<T> *r = NULL)
:data(x),rightChild(r),leftChild(l){}
};
二叉树的遍历有分前序遍历(根左右)、中序遍历(左根右)和后序遍历(左右根)。
而后序遍历的实例有:计算二叉树的结点个数,二叉树左子树的结点加上右子树的结点,并把访问根结点的操作为+1,用递归的方式进行计算。还可以计算二叉树的高度,具体方法为分别计算左子树和右子树的高度,取两者最大的值再+1。
template<class T>
int BinaryTree<T>::Size(BinTreeNode<T> *subTree)const {
if (subTree == NULL)return 0;
else return 1 + Size(subTree->leftChild) + Size(subTree->right);
}
template<class T>
int BinaryTree<T>::Height(BinTreeNode<T> *subTree)const {
if (subTree == NULL)return 0;
else {
int i = Height(subTree->leftChild);
int j = Height(subTree->rightChild);
return (i < j) ? j + 1 : i + 1;
}
}
前序遍历的应用有:实现二叉树的复制构造函数,先复制根结点,再复制左子树和右子树。还可以利用前序遍历来判断两棵树是否相同,也可以使用前序遍历,再使用中序遍历来判断是否两棵树相等,使用前序遍历和中序遍历可以唯一确定一棵树。
最小堆
最小堆的下滑调整算法
void MinHeap<E>::siftDown(int start, int m) {
int i = start, j = 2 * i + 1;
E temp = heap[i];
while (j <= m) { /j不能大于m,也就是不能调整到m下面了
if (j <= m && heap[j] > heap[j + 1])j++;/j要指向小的那一个结点
if (temp < heap[j])break;/左结点比当前结点大,满足最小堆则break
else { heap[i] = heap[j]; i = j; j = 2 * j + 1; }
}
heap[i] = temp; 记得回放temp中暂存的元素
}
最小堆是按层序遍历,从0开始排序的。其中j是i的左子树(想象一下假如序号为2的结点,其左子树的序号为多少?为5,满足j=2i+1这个公式)上滑调整算法类似
void MinHeap<E>::siftUp(int start) {
int j = start, i = (j - 1) / 2;
E temp = heap[j];
while (j > 0) {
if (heap[i] <= temp)break;
else { heap[j] = heap[i]; j = i; i = (i - 1) / 2; }
}
heap[i] = temp;
}
最小堆的插入是在最后插入,插入前判断是否堆满,插入之后进行从插入位置开始的siftUp操作,然后currentSize++。
最小堆的删除是删除堆顶元素,然后用最后一个元素来填补,currentSize - -。删除前判断是否堆空,然后用最后一个结点覆盖根结点,再进行siftDown(0,currentSize-1)的操作。
Huffman树
将给出的n个权值,构造n个树,然后选出其中权值最小的两棵树,构造一个新树,并且从F中删去原先的两棵树,加入新生成的树,如果F中只有一棵树,那么该树为Huffman树。Huffman树用一个最小堆来存放树,每棵树初始化时左右子树以及父结点均为NULL,再插入最小堆中。每次都从最小堆里取权值最小的两棵树合并形成一棵新树。
Huffman树的应用就是Huffman编码。意思指Huffman树往左走记为0,往右走记为1,这样生成的编码不会重复,并且使得最常用的字符的二进制编码最短(即占用的内存最小),离根结点最远的字符(即最不常用的字符)占用的二进制编码最长。
有序链表
链表本身是一种无序的数据结构,元素的插入和删除不能保证顺序性,但是有没有有序的链表呢?答案是肯定的,我们在单链表中插入元素时,只需要将插入的元素与头结点及其后面的结点比较,从而找到合适的位置插入即可。一般在大多数需要使用有序数组的场合也可以使用有序链表,有序链表在插入时因为不需要移动元素,因此插入速度比数组快很多,另外链表可以扩展到全部有效的使用内存,而数组只能局限于一个固定的大小中。
有序链表的合并
并查集
并查集书上定义的是一种集合,但我感觉其实更是一种树形结构,处理一些不会相交的集合。其中对并查集有两个方法,一个是Find,用来查找该元素的根结点,另一方法是Union,将两个集合合并为一个集合。每个集合中的根结点的值一开始均为-1,如果有结点指向自己,那么-1就继续减,比如说一个集合有三个结点,那么根结点的值为-3,如果结点有指向的结点,那么那个值就是其指向的结点的序号(故其值>=0,如果是<0那么一定是一个根结点,这一性质用于下面的Find方法)。
int UFSets::Find(int x) {
while (parent[x] >= 0)x = parent[x];
return x;
}
我们需要找到那个parent[x]为负的结点,也就是找到了根结点。
而Union方法则是形参传入两个集合,默认将第一个集合链接到第二个集合上(或是第二个链接到第一个上),那么这样会生成退化的树,使得Find方法效率太低。这个时候需要使用一个Size数组,用来标记两个集合所含结点数,将结点少的链接到结点多的集合。
但是通过Size也就是通过集合中结点数来判断还是不够,比如说一个集合只有一层,但是那一层有很多个结点,另一个集合只有四个结点,但是每个结点各为一层,那么如果是通过Size方法来链接,那么层数变多,效果并不理想。这个时候,需要另一种方法,也就是用rank数组。
rank数组记录了集合的层数,我们用层数少的链接层数多的。这样层数不会变(除非两个集合层数一样)。
我们还可以在Find方法上下功夫,也就是在查找父结点的过程中,将一些结点尽量上移,降低层数,也就是路径压缩。
具体实现就是先访问其上一层结点,如果parent[]的值>=0,则说明不为根结点,那么我们就将该结点链接到上上层结点(也就是其父结点的父结点)
int find(int p){
assert(p>=0&&p<count);/防止数组越界
while(p != parent[p]){//如果p元素的父亲指针指向的不是
//自己,说明p并不是集合中的根元素,还需要一直向上查找和路径压缩
//在find查询中嵌入一个路径压缩操作
parent[p]=parent[parent[p]];
//p元素不再选择原来的父亲节点,而是直接选择父亲
//节点的父亲节点来做为自己新的一个父亲节点
//这样的操作使得树的层数被压缩了
p=parent[p];//p压缩完毕后且p并不是根节点,p变成
//p新的父节点继续进行查找和压缩的同时操作
}
return p;//经过while循环后,p=parent[p],一
//定是一个根节点,且不能够再进行压缩了,我们返回即可
散列
依靠一个函数计算一个元素存储的位置,这种方法就是散列方法,而这个函数就是散列函数,构造出的表或结构就叫散列表(哈希表)。
散列表可以不必进行多次关键码的比较,搜索快,根据关键码通过散列函数得到唯一的存放地址。当然会引起存放地址的冲突,我们需要去减少冲突。
散列表
二叉搜索树
二叉搜索树指左子树关键码小于根结点,右子树大于根结点。因为这个性质,二叉搜索树的最小关键结点在树的最左下处,相反最大关键码在树的最右下处。
求二叉搜索树的前驱:如果结点左子树存在,那么前驱就是左子树的最大关键码(即最右下结点),如果没有左子树,那么这个时候:
1
1
1 该结点为其父结点的右孩子,那么前驱为该结点的父结点
2
2
2 该结点为父结点的左孩子,那么前驱为其最底层祖先
求二叉搜索树的后继:同寻找二叉搜索树的前驱类似,如果结点右子树存在,那么后继就是其右子树的最小关键码(即最左下结点),如果其右子树为空,则:
1
1
1 该结点为其父结点的左孩子,那么前驱为该结点的父结点
2
2
2 该结点为其父结点的右孩子,那么后继为该结点的最底层祖先
二叉搜索树的插入
插入一个新元素时,先检查该元素是否已经存在于二叉搜索树中。插入方法可以用递归的方式。
bool BST<E, K>::Insert(const E& e1, BSTNode<E, K> *& ptr) {
//该方法是在以ptr为根结点的二叉树中插入e1
if (ptr == NULL) {//树为空树
ptr = new BSTNode<E, K>(e1);//直接新生成结点并以e1初始化
if (ptr == NULL){cout << "Out Of space!\n"; exit(1);}
return true;
}
else if (e1 < ptr->data)Insert(e1, ptr->leftchild);
else if (e1 > ptr->data)Insert(e1, ptr->rightchild);
else return false;
}
二叉搜索树的删除
最简单的情况,便是删除叶结点,只需要将其父结点指向它的指针清空。如果结点只有一个左子树或右子树,那么只需要将该结点删去,再把断开的地方连起来(即父结点指向其孩子结点)。
简单的情况解决了,如果删除的结点左右子女都不为空,那么就寻找其左子树的最大关键码(或是右子树的最小关键码),然后去替换我们要删除的结点,然后这个最大关键码的位置又相当于一次删除(其实是一种递归)。
bool BST<E, K>::Remove(const K x, BSTNode<E, K> *& ptr) {
BSTNode<E, K> *temp;
if (ptr != NULL) {
if (x < ptr->data)Remove(x, ptr->right);
else if (x > ptr->data)Remove(x, ptr->left);
else if (ptr->left != NULL && ptr->right != NULL) {
temp = ptr->right;
while (temp->left != NULL)temp = temp->left;
ptr->data = temp->data;
Remove(ptr->data, ptr->right);
}
else {
temp = ptr;
if (ptr->left == NULL)ptr = ptr->right;
else ptr = ptr->left;
delete temp; return true;
}
}
return false;
}