c++数据结构与算法

目录

面向对象程序设计

复杂度分析

链表

单向链表

双向链表

循环链表

跳跃链表

稀疏表

栈和队列

栈:(后进的先出)

队列:(先进的先出)

用两个栈实现队列

用两个队列来实现栈

递归

阶乘的递归实现

迭代求n的阶乘

回溯

二叉树

基本概念:

二叉查找树的查找

广度优先遍历

深度优先遍历

树的平衡:

AVL树

图的概念:

图的表示方式

图的遍历

Dijkstra算法:

多源多目标的最短路径问题

生成树

排序

插入排序

选择排序

冒泡排序

堆排序

快速排序

归并排序

排序算法比较

散列

构造散列函数的方法

冲突解决方法

数据压缩

Huffman编码

面向对象程序设计

面向对象的四大特点:

  • 封装

  • 继承

  • 多态

  • 重载(如果问三大特点则不要重载)

1、封装: 类的封装就是将public的暴露出来,与其他类进行联系,便于修改,而把private的隐藏起来。

封装的好处

  • 数据和成员函数之间的结合更加紧密

  • 对象更便于查找错误

  • 对象可以对其他对象隐藏某些细节,从而使某些操作不受其他对象的影响。

2、继承:

  • 派生类继承了基类的所有成员,不需要重复定义

  • 公有继承则派生类与基类成员的属性一致,受保护继承则在派生类中所有成员都受保护,私有继承也是派生类的成员全部私有。

  • 派生类可以通过引入自己的定义重写成员函数的定义。(重写函数)

  • 派生类不一定只有一个基类,它可以从多个基类派生得来。

3.多态: 多态性指获得多种形态的能力。OPP中,多态性使用同样的函数名称表示多个函数,而这些函数是不同对象的成员。

多态的好处

  • 提高了代码的维护性

  • 增加了代码的可拓展性

4、标准模板库(STL):

需要知道的容器:

deque(双端队列), list, stack, queue, vector

该标准模板库包括3种类型的通用项:容器、迭代器和算法

  • tack,queue容器不支持选代器,

  • deque和vector可以支持几乎所有迭代器。

  • list不支持大于小于,不支持加减,不支持+=和-=”。

复杂度分析

算法复杂度可以根据时间或者空间复杂度来划分。 各个复杂度的代表算法:

链表

单向链表

插入&删除

​
// intSLList.h
class IntSLlNode  //节点类  包括储存的值和指向下一个节点的指针
{
  public:
    IntSLLNode()    
    {
        next=0;
    }
    IntSLLNode(int el,IntSLLNode *ptr)
    {
        info=el;
        next=ptr;
    }
    int info;
    IntSLLNode *next;
};
​
class IntSLList     //链表类
{
  public:
    IntLList()
    {
        head=tail=0;      //初始化头跟尾等于0
    }
    ~IntSLList();    //析构函数
    int isEmpty()
    {
        return head=0;
    }
    void addToHead(int); //在链表前面插入数据
    void addToTail(int);//在链表后面插入数据
    int deleteFromHead();//删除第一个节点并返回第一个节点的数据
    int deleteFromTail();//删除最后的节点并返回该节点的数据
    void deleteNode(int);//删除任意位置数据的节点
    bool isInList(int) const;//判断该数据是否在这个链表种
    private:
    IntSLLNode *head,*tail;
};
//intSLList.cpp
#include<iostream>
#include"intSLList.h"
using namespace std;
​
IntSLList::~IntSLList()
{
    for(IntSLLNode *p;!isEmpty(); )//逐一删除每一个节点
    {
        p=head->next;
        delete head;
        head=p;
    }
}
/*链表前面添加节点
(1)创建一个空节点
(2)将该节点的info成员初始化为特定的整数
(3)将该节点添加到链表的前面,用其next成员指向链表中的第一个节点的指针,也就是head当前的值
(4)head指针指向新节点*/
void IntSLList::addToHead(int el)
{
    head=new IntSLLNode(el,head);//先创建一个节点info为el,next指向链表的头节点,再让head指向el;
    if(tail==0)               //添加节点后判断链表是否只有一个节点的情况
        tail=head;
}
​
/*向链表末尾添加节点
(1)创建一个空节点
(2)将该节点的info成员初始化为整数el
(3)因为该节点要添加到链表的末尾,所以将它的next成员设置为null
(4)让链表最后一个节点next指向该节点
(5)tail指针指向该节点*/
void IntSLList::addToTail(int el)
{
    if(tail!=0) //如果链表不是空的话
    {
        tail->next=new IntSLLNode(el);//next成员为空所以不传入参数
        tail=tail->next;
    }
    else head=tail=new IntSLLNode(el);
}
​
/*删除前节点:
考虑两种特殊情况
(1)试图从空链表中删除一个节点
(2)待删除节点的链表只有一个节点(删除完之后就变成空链表,tail和head都为null)
假如删除的节点为空节点,有两种解决办法:
(1)使用assert语句(是程序终止)
(2)抛出异常
为了避免返回值是0的节点与空节点混淆,可以让函数返回一个指向整数的指针,而不是返回一个整数。
​
删除操作包括删除链表开头节点以及返回其中存储的值。*/
int IntSLList::deleteFromHead()
{
    int el=head->info;
    IntSLLNode *tmp=head;        //新建一个节点指向第一个节点,然后将head往后移动,再删除第一个节点
    if(head==tail) //链表中只有一个节点
        head=tail=0;
    else head=head->next;
    delete tmp;
    return el;
}
/*删除后节点:
(1)因为没有tail前驱节点,所以我们要通过遍历链表明直到找到tail的前驱节点。
(2)然后进行删除尾节点的操作
(3)将tail指向新的尾节点。*/
int IntSLList::deleteFromTail()
{
    int el =tail->info;
    if(head==tail)
    {
        delete head;
        head=tail=0;
    }
    else //该链表的节点数大于1
    {
        IntSLLNode *tmp;//找到tail的前哟个节点
        for(tmp=head;tmp->next!=tail;tmp=tmp->next);
        delete tail;
        tail=tmp;
        tail->next=0;
    }
    return 0;
}
/*删除特定值的节点:
(1)因为不确定该节点在哪个位置,所以首先遍历链表,找到该节点的位置
(2)遍历链表,找到该节点的前驱节点
(3)删除操作*/
void IntSLList::deleteNode(int el)
{
    if(!head=0) //如果链表不是空的
        if(head==tail&&el==head->info)//如果链表只有一个节点并且要删除的是这个节点
        {
            delete head;
            head=tail=0;
        }
        else if(el==head->info) //超过一个节点,然后要删除的数刚好是第一个节点
        {
            IntSLLNode *tmp=head;
            head=head->next;
            delete tmp;
        }
        else{
            IntSLLNode *pred,*tmp;
            for(pred=head,tmp=head->next;tmp!=0&&!(tmp->info==el);pred=pred->next,tmp=tmp->next);
            if(tmp!=0)
            {
                pred->next=tmp->next;
                if(tmp==tail)       //要删除的是最后一个节点
                    tail=pred;
                delete tmp;
            }
        }
}
bool IntSLList::isInList(int el)const
{
    IntSLLNode *tmp;
    for(tmp=head;tmp!=0&&!(tmp->info=e=l);tmp=tmp->next);
    return tmp!=0;
}

​

查找

遍历链表,用临时变量储存值与链表储存值比较,如果相等则退出循环

双向链表

该链表的每个节点都有两个指针,一个指向前驱,一个指向后继,称为双向链表

  • 插入后节点

    • 创建一个新的节点

    • 将新节点赋值

    • 新节点的next成员置为null

    • 将新节点的prev(指向前驱节点的指针)指向tail(指向原来的最后一个节点的指针)

    • 将tail指向新节点

    • 将前驱节点的next指向新节点

循环链表

单循环链表——在单链表中,将终端节点的指针与null改为指向表头节点或开始节点即可

循环链表的优点:

  • 遍历时终止循环不用判断指针为null(循环链表没有null)

  • 从任意一点出发可以到链表的任意另一点

跳跃链表

跳跃链表的查找

  • 从最上层的指针开始寻找,找到该元素就借书查找

  • 如果达到了该链表的末尾,或者遇到了比该元素要大的值key就从key前面的哪个节点进行查找,查找的时候从比之前低一级的指针开始

  • 循环以上过程,知道找到该元素为止

稀疏表

最好的方式时用两个一位链表数组

栈和队列

栈:(后进的先出)

栈的基本操作:

  • clear()——清空栈

  • isEmpty()——判断栈是否为空

  • push(e1)—-将元素e1放到栈的顶部

  • pop()——弹出栈顶部的元素(附带删除并且不获取该元素的值)

  • topEl()——获取栈顶部的元素,但是不删除。

​
//栈的向量实现
#include<vector>
template<class T,int capacity=30>
class Stack
{
public:
    Stack()
    {
        pool.reserve(capacity);
    }
    void clear()
    {
        pool.clear();
    }
    bool isEmpty()const
    {
        return pool.empty();
    }
    T& topEl()
    {
        return pool.back();
    }
    T pop()
    {
        T el=pool.back();
        pool.pop_back();
        return el;
    }
    void push(const T&el)
    {
        pool.push_back(el);
    }
    private:
    vector<T>pool;
};
//栈的链表实现
#include<list>
template<class T>
class LLStack
{
  public:
    LLStack()
    {
        
    }
    void clear()
    {
        lst.clear();
    }
    bool isEmpty()const
    {
        retuen lst.empty();
    }
    T& topEL()
    {
        return lst.back();
    }
    T pop()
    {
        T el=lst.back();
        lst.pop_back();
        return el;
    }
    void push(const T& el)
    {
        lst.push_back(el);
    }
    private:
    list<T>lst;
};

​

链表实现形式与抽象栈更匹配。

在向量和链表的实现形式中出栈和入栈操作的运行时间为长处O(1),但是在向量实现形式中,将一个元素压入已满的栈需要分配更多的存储空间,并且需要将现有向量中的所有元素复制到一个新的向量中。因此,最坏的情况下完成入栈操作需要花费O(n)时间

队列:(先进的先出)

队列的基本操作:

  • clear()——清空队列

  • isEmpty()——判断队列是否为空

  • enqueue(e1)——在队列的尾部加入元素e1

  • dequeue()——取出队列第一个元素

  • firstEI()——获取队列第一个元素,但是不删除

​
//队列的数组实现
template<class T,int size=100>
class ArrayQueue
{
  public:
    ArrayQueue()
    {
        first=last=-1;
    }
    void enqueue(T);
    T dequeue();
    bool isFull()
    {
        return first==0&&last==size-1||first==last+1;        //在数组中队列是满的两种情况
    }
    bool isEmpty()
    {
        return first==-1;
    }
    private:
    int first,last;
    T storage[size];    //实现队列的数组
};
template<class T,int size>
void ArrayQueue<T,size>::enqueue(T el)
{
    if(!isFull)
    {
        if(last==-1||last==size-1)  //如果链表是空的或者队列的最后一个元素在数组的最后一个位置
        {
            storage[0]=el;
            last=0;
            if(first==-1)
                first=0;
        }
        else storage[++last]=el;
    }
    else cout<<"Full queue\n";
}
template<Class T,int size>
T ArrayQUeue<T,size>::dequeue()
{
    T tmp;
    tmp=storage[first];
    if(first==last)  //队列只有一个元素
        last=first=-1;
    else if(first==size-1)
        first=0;
    else first++;
    return tmp;
}
//队列的链表实现
#include<list>
template<class T>
class Queue
{
  public:
    Queue()
    {
        
    }
    void clear()
    {
        lst.clear();
    }
    bool isEmpty()const
    {
        return lst.empty();
    }
    T& front()
    {
        return lst.front();
    }
    T dequeue()
    {
        T el=lst.front();
        lst.pop_front();
        return el;
    }
    void enqueue(const T& el)
    {
        lst.push_back(el);
    }
    private:
    list<T>lst;
};



​

用两个栈实现队列

栈1用来插入数据

栈2用来删除栈1插入进来的数据

用两个队列来实现栈

用一个队列放栈中储存的数据,然后删除最后一个数的前面所有数,删除的数尾插入队列2,然后队列1中的元素出队,这样实现后进先出。

递归

函数调用和递归实现:

每个函数(包括主函数)的状态都是由以下因素决定的:

  • 函数中所有局部变量的内容

  • 函数的参数值

  • 表明在调用函数的何处重新开始的返回地址

包含所有这些信息的数据区称为活动记录或者栈框架。

活动记录的寿命一般很短。在函数开始执行时得到动态分配的空间,在函数退出时释放其空间。

无论函数是由什么函数调用,都会在运行时栈中建立活动记录。运行时栈总是反映函数当前的状态。

递归调用不是表面上的函数调用自身,而是一个函数的实例调用同一个函数的另一个实例。这些调用在内部表示为不同的活动记录,并由系统区分。

阶乘的递归实现

double power(double x,unsigned int n)
{
    if(n==0)
        return 1.0;
    else
        return x*power(x,n-1);
}

迭代求n的阶乘

//迭代其实就是用循环的方法求阶乘
int main()
{
    int val,i,mult=1;
    cout<<"val="<<val<<endl;
    for(i=0;i<=val;i++)
        mult=mult*i;
    cout<<mutl<<endl;
    return 0;
}

回溯

//八皇后的伪代码
putQueen(row)
    for 同一行row上的每个位置col
        if 位置col可以放皇后
            将下一个皇后放在位置col处;
            if(row<8)
                putQueen(row+1);
            else 成功;
            取走位置col上的皇后

思路:

  • 设置一个8*8的数组代表棋盘,并且初始化所有值为1

  • 每当放入一个皇后,设置不能放皇后的位置为0

  • 回溯时,将这些设为0的位置重置为1(即可以放皇后)

二叉树

基本概念:

  • 树的根结点在最顶层,叶子节点在最底部

  • 空结果是一棵空树

  • 空树的高度时0

  • 单一节点时高度为1的树(节点既是根也是叶子)

非空树的高度是树中节点的最大层次

节点的层次是从根节点到该节点的路径的长度加1

第一层有一个节点,第二层有2个节点,第三层有4个节点,一直到第i层有

2^i个节点的树,称为完全二叉树

二叉查找树(有序二叉树)的特性:

对于树中的每个节点n,其左子树(根结点为左子节点的树)中的值小于节点n中的值v,其右子树中的值大于节点n中的值v。

二叉树的实现:

二叉树可以通过两种方式实现:

  • 数组(向量)

  • 链接结构

一个新的实现方式是用类来实现树

节点是一个类的实例,,该类由一个信息成员和两个指针成员组成。该节点由另一个类(该类将树作为整体对待)的成员函数使用并操作。

//genBST.h
#include<queue>
#include<stack>
using namespace std;
template<class T>
class Stack:public stack<T>
{
    
}
template <class T>
class Queue:public queue<T>
{
    T dequeue()
    {
        T tmp=front();
        queue<T>::pop();
        return tmp;
    }
    void enqueue(const T& el)
    {
        push(el);
    }
};
template<class T>
class BSTNode
{
    public:
    BSTNode()
    {
        left=right=0;
    }
    BSTNode(const T&e,BSTNode<T>*l=0,BSTNode<T>*r=0)
    {
        el=e;
        left=l;
        right=r;
    }
    T el;
    BSTNode<T> *left,*Right;
};
template<class T>
class BST
{
    public:
    BST()
    {
        root=0;
    }
    ~BST()
    {
        clear();
    }
    void clear()
    {
        clear(root);
        root=0;
    }
    bool isEmpty()const
    {
        return root==0;
    }
    void preorder()
    {
        preorder(root);
    }
    void inorder()
    {
        inorder(root);
    }
    void postorder()
    {
        postorder(root);
    }
    T* search(const T&el)const
    {
        return search(root,el);
    }
    void breadthFirst();   //从上到下、从左到右的广度优先遍历
    void iterativePreorder();     //前序遍历的非递归实现
    void iterativeInorder();      //中序遍历的非递归实现
    void iterativepostorder();    //后序遍历的非递归实现
    void insert(const T&);        //插入
    protected:
    BSTNode<T>* root;
    void clear(BSTNode<T>*);
    T* search(BSTNode<T>*,const T&)const;
    void preorder(BSTNode<T>*);
    void inorder(BSTNode<T>*);
    void postorder(BSTNode<T>*);
    virtual void visit(BSTNode<T>*p)
    {
        cout<<p->el<<' ';
    }
};

二叉查找树的查找

最坏的情况O(n)

template<class T>
T* BST<T>::search(BSTNode<T>*p,const T& el)const
{
    while(p!=0)
        if(el==p->el)
            return &p->el;
        else if(el<p->el)
            p=p->left;
        else p=p->right;
    return 0;
}

广度优先遍历

//从上到下、从左到右的广度优先遍历实现
template <class T>
void BST<T>::breadthFirst()
{
    Queue<BSTNode<T>*>queue; //定义一个类型为节点指针的队列
    BSTNode<T>* p=root;
    if(p!=0)
    {
        queue.enqueue(p);
        while(!queue.empty())
        {
            p=queue.dequeue();
            visit(p);
            if(p->left!=0)
                queue.enqueue(p->left);
            if(p->right!=0)
                queue.enqueue(p->right);
        }
    }
}

深度优先遍历

//中序遍历
template<class T>
void BST<T>::inorder(BSTNode<T>*p)
{
    if(p!=0)
    {
        inorder(p->left);
        visit(p);
        inorder(p->right);
    }
}
//前序遍历
template<class T>
void BST<T>::preorder(<BSTNode<T>*p)
{
    if(p!=0)
    {
        visit(p);
        preorder(p->left);
        preorder(p->right);
    }
}
//后序遍历
template<class T>
void BST<T>::postorder(BSTNode<T>*)
{
    if(p!=0)
    {
        postprder(p->left);
        postorder(p->right);
        visit(p);
    }
}
//前序遍历的非递归实现
template<class T>
void BST<T>::iterativePreorder()
{
    Stack<BSTNode<T>*>travStack;
    BSTNode<T>* p=root;
    if(p!=0)
    {
        travStack.push(p);
        while(!travStack.empty())
        {
            p=travStack.pop();
            visit(p);
            if(p->right!=0)     //栈是先进后出的所以先放右再放左
                travStack.push(p->right);
            if(p->left!=0)
                travStack.push(p->left);
        }
    }
}
//后序遍历的非递归实现
template<class T>
void BST<T>::iterativeinorder()
{
    Stack<BSTNode<T>*>travStack;
    BSTNode<T>* p=root,*q=root;
    while(p!=0)
    {
        for(;p->left!=0;p=p->left)
            travStack.push(P);
        while(p->right==0||p->right==q)
        {
            visit(p);
            q=p;
            if(travStack.empty())
                return;
            p=travStack.pop();
        }
        travStack.push(p);
        p=p->right;
    }
}
//中序遍历的非递归实现
template<class T>
void BST<T>::iterativeinorder()
{
    Stack<BSTNode<T>*>travStack;
    BSTNode<T>*p=root;
    while(p!=0)
    {
        while(p!=0)
        {
            if(p->right)
                travStack.push(p->right);
            travStack.push(p);
            p=p->left;
        }
        p=travStack.pop();
        while(!travStack.empty()&&p->right==0)
        {
            visit(p);
            p=travStack.pop();
        }
        visit(p);
        if(!travStack.empty())
            p=travStack.pop();
        else p=0;
            
    }
}

树的平衡:

树中任意一个节点的两个子树的高度差为0或者1,该二叉树就是高度平衡的,或者称为平衡的

平衡树的递归算法

template<class T>
void BST<T>::balance(T data[],int first,int last)
{
    if(first<=last)
    {
        int middle=(first+last)/2;
        insert(data[middle]);
        balance(data,first,middle-1);
        balance(data,middle+1,last);
    }
}

思路:

  • 找到中间节点((first+last)/2)作为根结点

  • 找到最前面的一个点与中间点的中间节点,作为左子节点

  • 再找中间点成为左子节点的左子节点

  • 一直循环知道取到最前面的点后,剩下的在根结点前面的点依次按顺序插入到每一层的剩下一个节点中

AVL树

堆是一种特殊类型的二叉树,具有以下两个性质:

  • 每个节点的值大于等于每个子节点的值

  • 该树完全平衡,最后一层的叶子都处于最左侧的位置

将堆作为优先队列

复杂度O(lgn)

  • 优先队列中添加元素

为了将元素入队,可以将元素作为最后一个叶节点添加到堆的末尾。为了保持堆的性质,在添加元素时可以将最后一个叶节点向根部移动

//伪代码
heapEnqueue(el)
    将el放在堆的末尾;
    while el不位于根部且el>parent(el)
        el与其父节点交换;

  • 优先队列中删除元素p从堆中删除元素,需要从堆中删除根元素,因为根据堆的性质,根元素的优先级最高。然后将最后一个叶节点放在根结点上,几乎肯定要恢复堆的性质,此时可以将根结点沿着树向下移动

//伪代码
heapDequeue()
    从根结点中提取元素;
    将最后一个叶节点中的元素放在要删除元素的位置;
    删除最后一个叶节点;
    //根的两个子树都是堆
    p=根结点;
    while p不是叶节点,并且p小于它的任何子节点
        交换p与其较大的子节点;
//将根元素沿树向下移动的算法实现
template<class T>
void moveDown(T data[], int first, int last)
{
    int largest = 2 * first + 1;
    while (largest <= last)
    {
        if (largest < last && data[largest] < data[largest + 1])
            largest++;
        if (data[first] < data[largest])
        {
            swap(data[first], data[largest]);
            first = largest;
            largest = 2first + 1;
        }
        else largest = last + 1;
    }
}

图的概念:

图分为:

  • 有向图

  • 无向图

边集:

  • 有向图:(v1,v2)

  • 简单图(无向图):{v1,v2}

环和回路的区别:

  • 环: 回路中所有顶点都不相同,叫做环

  • 回路: 在有向图中,从某点出发,不经过重复边,最终又能通过边回到该点。 例: A->B B->C A->C是环,但不是回路 A->B B->C C->A是环,也是回路

图的表示方式

图的遍历

深度优先遍历

​


DFS(V)
    num(V)=i++;    //先num(V)=1后,i再++
    for 顶点v的所有邻接点u     //对v的所有顶点进行循环访问
        if num(u)=0
            把边edge(uv)加入边集edges;
            DFS(u);
depthFirstSearch()
    for所有顶点v          
        num(v)=0;   //将所有的顶点编号为0
    edges=null;     //边的集合
    i=1;
    while存在一个顶点v使num(v)=0      //循环条件是该顶点还未被访问,编号还是0
        DFS(v);
    输出边集edges;

​

广度遍历

​
breadFirstSearch()
    for所有顶点u
        num(u)=0;        //将所有顶点编号为0
    edges=null;          //边的集合
    i=1;
    while存在一个顶点v使num(v)为0     //还有没被遍历的顶点
        num(v)=i++;
        enqueue(v);
        while队列非空
            v=dequeue();
            for顶点v的所有邻接点u  //先对顶点v的所有邻接点编号并入队
                if num(u)为0
                    num(u)=i++;
                    enqueue(u);
                    把边edge(vu)加入到edges中;
    输出edges;

​

​

Dijkstra算法:

操作步骤:

  • 首先,定义一个数组D,D[v]表示从源点s到顶点v的边的权值,如果没有边则将D[v]置为无穷大

  • 把图的顶点集合划分为两个集合S和V-S。第一个集合S表示距源点最短路径已经确定的顶点集合,即一个顶点如果属于集合S则说明从源点s到该点的最短路径已知。其余顶点放在另一个集合V-S中

  • 每次从尚未确定最短路径长度的集合V-S中取出一个最短特殊路径长度最小的顶点u,将u加入集合S,同时修改数组D中由s可达的最短路径长度。若加入集合S的u作为中间顶点时,vi的最短路特殊路径长度变短,则修改vi的距离值(即当D[u] + W[u, vi] < D[vi]时,令D[vi] = D[u] + W[u, vi])

  • 重复第 3 步的操作,一旦 S 包含了所有 V 中的顶点,D 中各顶点的距离值就记录了从源点 s 到该顶点的最短路径长度。

迪杰斯特拉最短路径

DijkstraAlgorithm(带权的简单有向图digraph,顶点first)
    for 所有顶点v
        currDist(v)=∞;
    currDist(first)=0;
    toBeChecked=所有顶点;    //V-S集合
    while toBeChecked非空
        v=toBeChecked中currDist(v)最小的顶点;
        从toBeChecked中删除v;
        for toBeChecked中v的所有邻接顶点u
            if currDist(u)>currDist(v)+weight(edge(vu))
                currDist(u)=currDist(v)+weight(edge(vu));
                predecessor(u)=v;

多源多目标的最短路径问题

查找某个任意顶点到其他任意顶点的最短路径

WFIalgorithm(矩阵weight)
    for i=1 to |V|      //i为中间点
        for j=1 to |V|      //j为起点
            for k=1 to |V|     //k为终点
                if weight[j][k]>weight[j][i]+weight[i][k]
                    weight[j][k]=weight[j][i]+weight[i][k]

图示

图示来源

生成树

克鲁斯卡尔实现生成树步骤:(归并边,从最小的边开始,如果不构成环的边就加入树中)

  • 按权重从大到小排列边

  • 从最小权重边的两个顶点开始建立树

  • 若加入的边不连通,则合并两棵子树(注意:联通的本质是寻找两个子树是否有同一父节点,在图中指是否有共同最大连通点,如v4->v7,v6->v7,当v4和v6连接时,因其有共同父节点构成连通)

  • 从剩余最小边合并子树

KruskalAlgorithm(加权连通无向图graph)
    tree=null;
    edges=graph中所有按边权值排序的序列;
    for(i=1;i<|E|且|tree|<|V|-1;i++)
        if edges中的ei不能与tree中的边构成环
            将ei加入tree;

迪杰斯特拉实现生成树

  • 逐个将边加到树中,如果检测到环,则删除环中权值最大的边

DijkstraMethod(加权连通无向图graph)
    tree=null;
    edges=graph中所有边的一个未排序序列;
    for j=1 to |E|
        将ei加入到tree中;
        if tree中有环
            从环中删除权值最大的边;

排序

插入排序

1.先比较前两个数

2.然后考虑第三个数、四、五....

//先用外层循环获取需要比较的数data[i](tmp=data[i]),然后内层循环让j等i当前位置,将该数(tmp)与前面的数(data[j-1])逐一比较,若tmp小于前面的数就将该数复制到下一个位置上,然后将tmp放到合适的位置data[j]

//伪代码
insertionsort(data[],n)
    for i=1到n-1
        将大于data[i]的所有元素都移动一个位置,将data[i]放到合适的位置上
//代码
template<class T>
void insertionsort (T data[],int n)
{
    for(int i=1;i<n;i++)
    {
        T tmp=data[i];
        for(int j=i;j>0&&tmp<data[j-1];j--)
            data[j]=data[j-1];
    	data[j]=tmp;
    }
}

选择排序

先找出数组中的最小元素,将其与第一个位置上的元素进行交换,然后在剩余元素data[1],...data[n-1]中找到最小的元素,把它放在第二个位置上

//伪代码
selectionsort(data[],n)
    for i=0到n-2
        找出元素data[i],...,data[n-1]中的最小的元素;
        将其与data[i]交换
//代码
template<class T>
void selectionsort(T data[],int n)
{
    int min;
    for(int i=0;i<n-1;i++)
    {
        for(int j=i+1,min=i;j<n;j++)      //找出最小值的下标
            if(data[j]<data[min])
                min=j;
        swap(data[min],data[i]);
    }
            
}

冒泡排序

数组从最后两个数开始比较(data[n-1]与data[n-2]),若逆序,则交换两个数,接着data[n-2]与data[n-3],同理一直到data[1]和data[0],这样最小的数就到了数组的顶部

//伪代码
bubblesort(data[],n)
    for int i=0到n-2
        for int j=n-1往下到i+1
            如果两者逆序,则交换位置j和位置j-1的元素
//代码
template<class T>
void bubblesort(T data[],int n)
{
    for(int i=0;i<n-1;i++)
    {
        for(int j=n-1;j>i;j--)
            if(data[j]<data[j-1])
                swap(dara[j],data[j-1]);
    }
}
 
//冒泡排序改进   如果某次没有发生交换,就会停止排序这个过程
template<class T>
void bubblesort(T data[],const int n)
{
    bool again=true;
    for(int i=0;i<n-1&&again;i++)
    {
        for(int j=n-1,again=false;j>i;j--)
            if(data[j]<data[j-1])
            {
                swap(dara[j],data[j-1]);
                again=true;
            }                
    }    
}

堆排序

堆时具有一下两个属性的二叉树

1、每个节点的值不会小于其子节点的值

2、树是完全平衡的,最底层的叶子节点都位于最左边的位置上

堆中的元素不会完全按序排列,唯一肯定的是最大的元素位于根结点。堆排序从堆开始,将最大的元素放在数组的末尾,然后重建少了一个元素的堆.....

//堆排序伪代码
heapsort(data[],n)
    将数组data转化为堆
        for i=down to 2
        将根结点与位置i的元素交换位置
        恢复树data[0],...data[i-1]的堆属性
            
//代码
//
template<class T>
void moveDown(T data[],int first,int last)
{
    int c1=2*first+1;
    while(c1<=last)
    {
        if(c1<last&&data[c1]<data[c1+1])
            c1++;
        if(data[first]<data[c1])
        {
            swap(data[first],data[c1])
            first=c1;
            c1=2*first+1;
        }
        else c1=last+1;
    }
}
​
template<class T>
void heapsort(T data[],int size)
{
    for(int i=size/2-1;i>=0;--i)
        moveDown(data[],i,size-1);
    for(int i=size-1;i>=1;--i)
    {
        swap(data[0],data[i])
        moveDown(data,0,i-1);
    }
}

快速排序

原始数组划分成两个子数组,第一个子数组中的元素小于或等于一个选定的关键字,这个关键字称为边界或基准,第二个子数组大于或等于这个边界值。递归

​
​quicksort(array[])
    if length(array)>1
        选择bound;  //将array划分为子数组subarray1和subarray2
        while array中还有元素
            在subarray1={el:el<=bound}中包含element;
            或者在sybarray2={el:el>=bound}中包含elemen;
        quicksort(subarray1);
        quicksort(subarray2);
//选取位于数组中间位置的元素作为边界值
​
template<class T>
void quicksort(T data[],int first,int last)
{
    int lower=first+1,upper=last;
    swap(data[first],data[(first+last)/2]);
    T bound=data[first];
    while(lower<=upper)
    {
        while(data[lower]<bound)
            lower++;
        while(bound<data[upper])
            upper--;
        if(lower<upper)
            swap(data[lower++],data[upper--]);
        else lower++;
    }
    swap(data[first],data[upper]);
    if(first<upper-1)
        quicksort(data,first,upper-1);
    if(upper+1<last)
        quicksort(data,upper+1,last);
}

​

​
//实现快速排序需要对该数组进行预处理:找出数组中最大的元素,将它与数组中的最后一个元素互换,将最大的元素放在数组末端,可以防止索引lower的值超过数组的末端
template<class T>
void quicksort(T data[],int n)
{
    int i=max;
    if(n<2)
        return;
    for(i=1,max=0;i<n;i++)
        if(data[max]<data[i])
            max=i;
    swap(data[n-1],data[max]);
    quicksort(data,0,n-2);
}

归并排序

伪代码
mergesort(data[],first,last)
    if first<last
        mid=(first+last)/2;
        mergesort(data,first,mid);     //data的左半部分
        mergesort(data,mid+1,last);    //data的右半部分
        merge(data,first,last);       ///所有的部分合并为一个排好的数组
​
​
merge(array1[],first,last)      //合并
    mid=(first+last)/2;
    i1=0;
    i2=first;
    i3=mid+1;
    while array1的左子树组和右子数组都包含元素
        if array1[i2]<array1[i3]
            temp[i1++]=array1[i2++];
        else temp[i1++]=array1[i3++];
    将array1中的剩余元素导入temp;
    将temp中的内容导入array1;

排序算法比较

排序算法最坏情况平均情况最好情况空间复杂度是否稳定
插入排序O(n²)O(n²)O(n)O(1)
选择排序O(n²)O(n²)O(²)O(1)
冒泡排序O(n²)O(n²)O(n)O(1)
堆排序O(nlog2n)O(nlog2n)O(nlog2n)O(1)
快速排序O(n²)O(nlog2n)O(nlog2n)O(log2n)
归并排序O(nlog2n)O(nlog2n)O(nlog2n)O(n)

散列

需要找到一个函数h,它能把特定的关键字K(可能是一个串、数字或者记录)转换成表的一个索引,该表用于存储与K同类型的项。这个函数h就称为散列函数。(用散列函数对关键字进行计算后得到一个下标(数组),把相关的信息放入该数组对应的位置)

散列函数可能会把两个或两个以上的不同关键字映射到同一地址,称这种情况为“冲突”,这些发生碰撞的不同关键词称为同义词

散列函数的好坏取决于其避免冲突发生的能力

构造散列函数的方法

  • 除余法

最简单的方法是除余法(使用取模运算):TSize=sizeof(table),而h(K)=K mod TSize,TSize最好是素数

  • 折叠法

    • 移位折叠

      如社会安全号码123-45-6789分割成三部分——123、456、789,然后把这三部分相加,得到结果1368,再对TSize进行取模运算

    • 边界折叠

      如社会安全号码123-45-6789分割成三部分——123、654、789,把这三部分相加,得到结果1566然后取模运算。

  • 平方取中法

关键字被平方后,将平方结果的中间部分作为地址。如果关键字是字符串,则必须先进行处理以产生数字

  • 提取法

只使用一部分关键字来计算地址。对于社会安全号码123-5-6789,只使用前四个数字1234,也可能只使用最后四个数字6789.

  • 基数转换法

基数转换法将关键字K转换为另一种数字基数,然后进行取模运算

  • 全域散列法

冲突解决方法

  • 开放定址

    • 线性探测

    • 二次探测法

    • 双散列

  • 链接法                                                                                                                                      关键字不必放在表中,在链接法中,表中的每个地址都关联着一个链表或者链结构,其中的info域用来保存关键字或者这个关键字的引用。                                                                           将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词子表,不是同义词放在多个位置了,而是把他们集中起来,再散列表里只存储所有同义词子表的头指针

链接法对于可能会造成很多冲突的散列函数来水,提供了接不会出现找不到地址的保障。当然,这也会增加查找时所需要遍历单链表的性能损耗

  • 桶定址

    把冲突的元素都放在表中的同一个位置中,因此可以为表中每个地址都关联一个桶(足够大的储存空间),该方法并不能完全避免冲突。

数据压缩

Huffman编码

定义:

给定个叶子节点,构造一棵二叉树,若这棵二叉树的带权路径长度达到最小,则称这样的二叉树为最优二叉树,也称为Huffman树

我认为讲解Huffman比较清楚的一个视频

  • 5
    点赞
  • 75
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值