数据结构复习

单链表
单链表的类定义(三种方法)

  1. 复合类
class List;//List类前视声明

class LinkNode {
	friend class List;//声明List类为友元类
private:
	int data;
	LinkNode *link;
};
class List {
public:
	//公操作部分
private:
	LinkNode *first;
};
  1. 嵌套类
class List {
public:
	//公操作部分
private:
	class LinkNode {//嵌套的类
	public:
		int data;
		LinkNode *link;
};
	LinkNode *first;//单链表的头指针
};
  1. 基类和派生类
class LinkNode {
protected:
	int data;
	LinkNode *link;
};

class Link :public class LinkNode {
private:
	LinkNode *first;
public:
	//链表相关操作
};
  1. 用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;
}
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值