数据结构与算法(每天更新)

常用数据结构

数据结构分类

数据结构描述了数据元素之间的关系,通常分为集合、线性、树型、图型。集合描述了对象间没有直接的关系,线性指之间关系一对一,树型指的是一对多,图型指的多对多。数据结构常见的有表、队列、栈、树、散列、堆、图,每种数据结构都有多种表示,有的操作代码编写简单,有的编写麻烦。类如普通的二叉树的一些添加、删除操作比较简单,但是对于平衡树的这些操作,由于需要考虑平衡,导致代码编写困难。还有一些数据结构的编写容易,但是需要一些算法解决子问题。类如散列表,通常使用的是拉链法构建散列表,但是其中求散列值是个需要仔细设计的问题。另外对于不同的数据结构,可以使用不同的物理结构,类如表可以用数组也可以用链表来表示,这些导致不同数据结构的相同操作的时间复杂度不一样,而且数组并不一定优于链表,链表也不一定由于数组。让人遗憾的是,它两没有绝对胜出的,数组在排序的情况下很方便检索,时间复杂度在O(lgn),而链表排序情况下也需要O(n)的时间,虽然是线性的,但在元素几百万、几千万后两者区别可以明显看出来。这个不能说明数组就比链表好,在插入与删除的操作上,链表又比数组优,所以选择什么物理实现,必须具体问题具体分析,必要的时候可以进行实验测试,看哪个运行时间快。下面对这几种数据结构做一个快速的分析。

表可以用数组与链表表示,数组表示代码如下:

#include 
#include 
#include 
template <class T>
class Array{
public:
    Array(){
        m_arr = new T[DEFAULT_SIZE]; assert(m_arr != NULL);
    }
    Array(size_t capacity):m_size(0), m_capacity(capacity){
        m_arr = new T[capacity];
        assert( m_arr != NULL);
    }
    T operator[](int index) const{
        assert(index <= m_size);
        return m_arr[index];
    }
    void insert(int pos, T value){
        assert(pos >=0 && pos <= m_size);
        this->resize();
        memmove(m_arr + pos + 1, m_arr + pos, sizeof(T) * (m_size - pos));
        m_arr[pos] = value;
        m_size++;
    }
    void add(T element){
        this->resize();
        m_arr[m_size++] = element;
    }

private:
    void resize(){
        if (m_size + 1 <= m_capacity) {
            return;
        }
        T *tmp = new T[2 * m_capacity];
        assert(tmp);
        memcpy(tmp, m_arr, m_size * sizeof(T));
        delete m_arr;
        m_arr = tmp;
        m_capacity *= 2;
    }

private:
    enum {DEFAULT_SIZE = 8};
    T *m_arr;
    size_t m_size;
    size_t m_capacity;
};

void testArrayCase(){
    Array<int> arr;
    arr.add(100);
    arr.add(200);
    arr.insert(0, 300);

    printf("%d\n", arr[0]);
    printf("%d\n", arr[1]);
    printf("%d\n", arr[2]);
}
输出:
300
100
200
Program ended with exit code: 0

上面使用了c标准库的断言,内存操作以及输出,重载了[]运算符,使用c++的模板进行编程,后面的代码都会对模板进行编程,可能有些类型需要进行特化,后面将引入迭代器。上面的testArrayCase是一个测试用例,写的不全。上面用数组实现的表提供了插入、添加、获取的外部接口。这个结构只需要三个遍历存储一些必要信息,有数组的指针、数组当前元素个数、数组容量。还需要在数组进行插入与添加时进行容量检查,如果容量不够,就重新申请一块大一倍的内存。上面的memmove是安全的内存移动,在进行移动时,会把原内存拷贝一份,然后再移动,所以不会出现覆盖之前元素的情况。需要注意的是,数组每次插入或者添加导致重新申请内存再拷贝之前的数据的时间复杂度是O(n),最坏情况之前N个数都需要进行后移。

下面是链表实现代码:

#include <assert.h>
#include <string.h>
#include <stdio.h>
template <class T>
struct Node{
    Node(T _data) : next(NULL), pre(NULL), data(_data){}
    Node *next;
    Node *pre;
    T data;
};

template <class T>
class List{
public:
    List() : m_elements(NULL){}
    Node<T>* add(T data){
        Node<T> *element = new Node<T>(data);
        assert(element);

        if (!m_elements) {
            m_elements = element;
            element->next = element;
            element->pre = element;
        }
        else{
            element->next = m_elements;
            element->pre = m_elements->pre;
            m_elements->pre->next = element;
            m_elements->pre = element;
            m_elements = element;
        }

        return element;
    }

    void insert(Node<T> *p, T data){
        assert(p);
        Node<T> *element = new Node<T>(data);
        assert(element);
        element->next = p;
        element->pre = p->pre;
        p->pre->next = element;
        p->pre = element;
    }

    Node<T>* begin(){ return m_elements;}
    Node<T>* next(Node<T>* p){
        assert(p);
        p = p->next;
        if (p == m_elements) {
            return NULL;
        }
        return p;
    }
    Node<T>* end(){ return NULL; }

private:
    Node<T> *m_elements;
};

void testListCase(){
    List<int> l;
    l.add(20);
    l.add(30);
    Node<int> *pp = l.add(50);
    l.add(70);
    l.insert(pp, 100);

    Node<int> *p;
    for (p = l.begin(); p != l.end(); p = l.next(p)) {
        printf("%d\n", p->data);
    }
}

上面的是双向循环链表,没有使用表头作为结束占位符。上面同样实现了添加与插入,数组实现的插入提供的参数是元素位置,然后把元素插入这个位置,链表是在节点的前面插入。这里提供了获得链表第一元素、链表尾,还有后移到下一个元素。实现的接口不太漂亮,原因在于并没有使用表头节点作为占位符表示结束,所以这里不得不end()返回NULL,不然返回这个占位符就可以了。

未完待续,持续更新中。

队列

#include 
#include 
#include 

template<class T>
class Queue{
public:
    Queue(){
        m_arr = new T[DEFAULTSIZE];
        m_head = m_tail = 0;
        m_capacity = DEFAULTSIZE;
    }
    ~Queue(){
        if (m_arr) {
            delete m_arr;
        }
    }
    void enqueue(T elem){
        if ((m_tail + 1) % m_capacity == m_head) {//queue is full
            this->resize();
        }
        m_arr[m_tail] = elem;
        m_tail = ++m_tail % m_capacity;
    }
    T dequeue(){
        assert(m_head != m_tail);//queue is empty
        int tmp = m_head;
        m_head = ++m_head % m_capacity;
        return m_arr[tmp];
    }
    void resize(){ // resize queue size
        T *tmp = new T[m_capacity*2];
        assert(tmp);
        for (int i=m_head, j=m_head; i!=m_tail; i=++i%m_capacity, j++) {
            tmp[j] = m_arr[i];
        }
        // tail is last position,it is used to check queue is full
        m_tail = m_head + m_capacity - 1; 
        m_capacity *= 2;
        delete m_arr;
        m_arr = tmp;
    }
private:
    enum {DEFAULTSIZE = 8};
    T *m_arr;
    int m_head;
    int m_tail;
    int m_capacity;
};

void testQueueCase(){
    Queue<int> q;
    for (int i=0; i<100; i++) {
        q.enqueue(i);
    }

    for (int i=0; i<100; i++) {
        printf("%d\n",q.dequeue());
    }
}

上面是用数组实现的循环队列,大小自动扩展,最后一个元素的下一个位置是哨兵,用来判断队列是否满了,所以队列满的时候会有一个位置不盛放元素,而作为队尾,当
(m_tail+1)%m_capacity==m_head时表示队列满了,当m_tail==m_head时表示队列为空。

排序算法

你可能感兴趣的:(数据结构与算法,数据结构,算法,c++,模板编程,表)