数据结构与算法(C)

194 阅读33分钟

Algorithms (tutorialhorizon.com)

线性表

线性表定义

线性表的特点

  1. 有序性
  2. 有穷性,由有限个数据元素组成,表长就是表中数据元素的个数
  3. 同一性。数据元素都是同一数据类型

顺序表

用一组地址连续的存储单元顺序存放线性表的各个数据元素(顺序存储结构)。

#include <iostream>
#define MAXSIZE 100
typedef int ElemType;

typedef struct {
    ElemType data[MAXSIZE]; 
    int length; //length+1为表长
}seqList;

// 1 <= i <= list.length
int getElem(seqList* L, int i, ElemType* e) {
    if(L->length == 0 || i < 1 || i > L->length)
        return 0;
    
    *e = L->data[i - 1];
    return 1;
}
int insert_list(seqList* L, int i, ElemType e) {
    int k;
    //顺序表已满
    if(L->length == MAXSIZE)
        return 0;
    //当i不在范围内
    if(i < 1 || i > L->length + 1)
        return 0;
    // 若插入位置不在表尾
    if(i <= L->length) {
        //将要插入位置后的数据元素后移一位
        for(k = L->length - 1; k >= i - 1; k--) {
            L->data[k + 1] = L->data[k];
        }
    }
    L->data[i-1] = e; //将新元素插入
    L->length++; //表长增1 
    return 1;
}

int delete_list(seqList* L, int i, ElemType* e) {
    int k;
    //线性表为空
    if(L->length == 0)
        return 0;
    //删除位置不正确
    if(i < 1 || i > L->length)
        return 0;
    
    *e = L->data[i-1];
    
    //如果删除的元素不在最后位置
    if(i < L->length) {
        //将删除位置后继元素前移
        for(k = i; k < L->length; k++) {
            L->data[k-1] = L->data[k];
        }
    }
    L->length--;
    return 1;
}

链表

单链表

#include <stdio.h>

//抽象数据类型
typedef int Data;


typedef struct _Node {  //结点
    Data data;
    struct _Node* next;     
}Node;

typedef struct _LinkList {
    Node* front; //指向头结点的指针
    Node* tail; //指向尾结点的指针
    int size;   //链表的大小
}LinkList;

//创建结点
Node* createNode(Data data) {
//1. 动态初始化新节点
//2. 判断新节点是否有足够的内存空间
//3. 如果新节点有足够的内存空间,就令新结点的next指向空, 将数据赋值在新节点上
    Node* newNode = (Node*)malloc(sizeof(Node));
    if(newNode != NULL)
    {
        newNode->next = NULL;
        newNode->data = data;
    }
    
    
        return newNode;
}

//判断链表是否为空
bool empty(LinkList* list)  {
    return list->size == 0;
}

//创建链表
LinkList* createList() {
    //1.动态分配
    LinkList* list = (LinkList*)malloc(sizeof(LinkList));
    
    //2.如果list为空
    if(list == NULL)
        return NULL;
    
    //3.首尾指针指向NULL
    list->front = NULL;
    list->tail = NULL;
    
    //4.链表元素初始化为0
    list->size = 0;
    
    return list;
}

//插尾
void push_tail(LinkList* list, Data data) {
    Node* newNode = createNode(data);
    
    if(newNode == NULL)
        return;
    
    if(empty(list)) {
        list->front = newNode;
    } else {
        //让尾节点连上新节点
        list->tail->next = newNode;
        //让尾指针指向新节点
    }
     list->tail = newNode;
     list->size++;   
}
//头插法
void push_front(LinkList* list, Data data) {
    Node* newNode = createNode(data);
    
    if(newNode == NULL)
        return;
    
    //如果链表为空
    if(empty(list)) {
        list->front = newNode;
        list->tail = newNode;
        
    } else {
        newNode->next = list->front;
        list->front = newNode;
    }
        list->size++;   
}

//输出、查找、删除要遍历
//给用户提供一个接口
void transform(LinkList* list, void (*func)(Data*)) {
    Node* curNode = list->front;    
    
    while(curNode != NULL) {
        func(&curNode->data);
        curNode = curNode->next;
    }
}

//输出链表
void traverse(LinkList* list) {
//1. 如果链表为空,就退出程序
//2. 链表不为空,就定义一个指针curNode,让它指向头结点
//3. 只要指针curNode不指向NULL, 就打印当结点的值
//4. 让curNode不断往后移
    if(empty(list))
        return;
    
    Node* curNode = list->front;
    while(curNode != NULL) {
        printf("%d ", curNode->data);
        curNode = curNode->next;
    }
}
void print(Data* data) {
    printf("%d ", *data * 2);
}

int main()
{
    LinkList* list = createList();
    
    for(int i = 0; i < 5; i++) {
        push_tail(list, i);
    }
    
    transform(list, print);

    return 0;
}

在链表中删除值为x的元素

  1. 定义两个指针变量, prev(前驱结点)和find(值为x的结点,要查找的结点)
  2. 令find = 头结点的下一个结点,也就是说从第二个结点开始找
  3. 定义一个循环,条件:查找的结点的值 不等于 x, 就让2个指针同时移动
  4. 如果x的值刚好等于find结点的值, 就令前驱结点的后继结点等于find结点的后继结点
Node* delete_list(Node* head, int x) {
    Node* prev; //前驱结点
    Node* find; //要查找的结点
    
    find = head->next; 
    
    //查找值为x的元素
    while(find->data != x) {
        //让2个指针同时移动
        prev = find;
        find = find->next;
    }
    find->next = prev->next;
    delete find;
    
    return head;
}

求带头结点链表的表长

  1. 定义一个指针变量cur
  2. 初始化整型变量i为0
  3. 令cur指向链表的头结点
  4. 循环 条件:cur不为空 操作: 令i自增,cur指针往后移
int Length_List(Node* head) {
    Node* cur = head;
    int i = 0;
    
    while(cur != NULL) {
        i++;
        cur = cur->next;
    }
    return i;
}

在单链表list中查找第i个元素的结点(按序号查找单链表数据元素)

  1. 定义一个指针变量 cur
  2. 初始化整型变量j为1
  3. 令cur指向list中的第二个结点,从第二个结点开始找
  4. 循环 条件:cur不为空且 j < i 操作:cur往后移,j累加
  5. 返回cur
Node* search(Node* list, int i) {
    Node* cur = list->next;
    int j = 1;
    while(cur != NULL && j < i) {
        cur = cur->next;
        j++;
    }
    return cur;
}

在单链表list中查找值为x的结点(按值查找单链表数据元素)

  1. 定义一个指针变量cur
  2. 让cur指向list的第二个结点
  3. 循环 条件:cur不为空且cur指向的结点的值不等于x 操作:cur往后移
  4. 返回cur
  Node* search(Node* list, int x) {
      Node* cur = list->next;
      
      while(cur != NULL && cur->data != x) {
          cur = cur->next;
      }
      return cur;
  }

单链表中删除相同的值 解题思路:先取头结点的值,将它与后面的所有结点值一一比较,发现相同的就删除掉,然后再取第二个结点的值,重复上述过程直到最后一个结点

解题步骤:

  1. 定义三个指针变量, *p, *q, *r

  2. 让指针p指向头结点

  3. 外层循环 条件(指针p不为空)
    操作: 将指针p所指向的地址赋值给q 内层循环 条件(指针q的后继结点不为空) 操作: 条件(指针q的后继结点的数据与指针p结点的数据相同) 指针r指向q的后继结点的数据 q的后继结点指向 r的后继节点 删除r(删除重复结点) 否则 q往后走

    内层循环结束
    

    p往后走 外层循环结束

    Node* p = NULL;
    Node* q = NULL;
    Node* r = NULL;
    
    p = head;
    
    while(p != NULL) {
        q = p;
        
        while(q->next != NULL) {
            if(q->next->data == p->data) {
                r = q->next;
                q->next = r->next;
                delete r;
            } else
                q = q->next;
        }
        p = p->next;
    }
}

双向链表

插头

  1. 动态分配新节点newNode
  2. 检查溢出条件,即判断内存是否可用于新结点, 内存不可用,显示溢出消息,可用,就为新节点分配内存
  3. 为新节点分配数据
  4. 判断双向链表是否为空 4.1: 如果双向链表为空(无结点),令newNode成为头结点, 且newNode的前驱和后继指针指向NULL
    4.2: 如果双向链表不为空, 令新结点的后继指针指向head,head的前驱指针指向新节点,最后令新节点成为头结点。
#include <iostream>

typedef struct _Node {
    int data;
    struct _Node* next;
    struct _Node* prev;
}Node;

void insert_front(Node** head, int data) {
    //先动态分配一个新节点
    Node* newNode = (Node*)malloc(sizeof(Node));
    
    //判断溢出条件
    if(newNode == NULL) {
        printf("Overflow");
        return;
    } else {
        //内存分配给新结点
        //赋值
        newNode->data = data;
        
        //判断双向链表是否为空
        if(*head == NULL) {
            //让新节点的前驱和后继指向NULL
            newNode->prev = NULL;
            newNode->next = NULL;
            //让新节点成为头结点
            *head = newNode;
        } else {
            //双向链表不为空
            newNode->next = *head;
            (*head)->prev = newNode;
            newNode->prev = NULL;
            *head = newNode;
        }
    }
}
//遍历双向链表
void traverse(Node* head) {
    //判断双向链表是否为空
    if(head == NULL) {
        printf("空链表");
        return;
    }
    Node* cur = head;
    while(cur != NULL) {
        printf("%d ", cur->data);
        cur = cur->next;
    }
}

插尾

  1. 动态分配新节点newNode
  2. 检查溢出条件,即判断内存是否可用于新结点, 内存不可用,显示溢出消息,可用,就为新节点分配内存
  3. 为新节点分配数据
  4. 判断双向链表是否为空 4.1: 如果双向链表为空(无结点),令newNode成为头结点, 且newNode的前驱和后继指针指向NULL
    4.2: 如果双向链表不为空,定义一个指针变量lastNode, 通过循环遍历到最后一个结点(注意边界条件, lastNode->next != NULL),令lastNode的后继指针指向newNode, newNode的前驱指针指向lastNode, newNode的后继指针指向NULL
    //先动态分配一个新节点
    Node* newNode = (Node*)malloc(sizeof(Node));
    
    //定义一个指针变量lastNode,用于遍历到最后一个结点
    Node* lastNode = NULL;
    
    //判断溢出条件
    if(newNode == NULL) {
        printf("Overflow");
        return;
    } else {
        //内存分配给新结点
        //赋值
        newNode->data = data;
        
        //判断双向链表是否为空
        if(*head == NULL) {
            //让新节点的前驱和后继指向NULL
            newNode->prev = NULL;
            newNode->next = NULL;
            //让新节点成为头结点
            *head = newNode;
        } else {
            //双向链表不为空
            lastNode = *head;
            // lastNode指向最后一个结点
            while(lastNode->next != NULL) {
                lastNode = lastNode->next;
            }
            newNode->prev = lastNode;
            newNode->next =  NULL;
            lastNode->next = newNode;
        }
    }
}

删头

  1. 检查链表是否为空, 如果链表为空,打印underflow下溢条件
  2. 如果链表只有一个结点, 令头结点指向空,释放掉头结点
  3. 如果链表有多个结点,定义一个指针变量cur, 令cur存储指向第一个结点(即头结点)的地址, 头结点往后移,头结点的前驱指针指向NULL,最后释放掉cur所存储的结点的地址
    Node* cur = *head;
    
    if(*head == NULL) {
        printf("Underflow");
        return;
    } else if((*head)->next == NULL && (*head)->prev == NULL) {
        *head = NULL;
        free(*head);
    } else {
        *head = (*head)->next;
        (*head)->prev = NULL;
        free(cur);
    }
}

删尾

  1. 检查链表是否为空, 如果链表为空,打印underflow下溢条件
  2. 如果链表只有一个结点, 令头结点指向空,释放掉头结点
  3. 如果链表有多个结点,定义一个指针变量cur, 通过循环让cur指向最后一个结点的地址(待删除结点),令cur的前驱结点的后继指针指向NULL,cur指针指向NULL,最后释放掉cur, 就删除最后一个结点了
void delete_rear(Node** head) {
    Node* cur = *head;
    
    if(*head == NULL) {
        printf("Underflow");
        return;
    } else if((*head)->next == NULL && (*head)->prev == NULL) {
        *head = NULL;
        free(*head);
    } else {
        //令cur指向最后一个结点
        while(cur->next != NULL) 
            cur = cur->next;
        
        cur->prev->next = NULL;
        cur = NULL;
        free(cur);
    }
}

栈具有后进先出的特点,限定元素的操作只能在表的一端(栈顶)进行。

栈和队列都有顺序或链式两种存储方式。

栈的主要操作是入栈和出栈。顺序栈由于受到期初设定的栈容量的限制,入栈操作时,必须进行判满操作,以免发生上溢。而链栈只有当无法动态申请到空间时,才无法入栈。

数组栈

#include <iostream>
#define MAXSIZE 100

typedef struct {
    int data[MAXSIZE]; //栈中的元素通过数组来存储
        int top;//栈顶指针
}seqStack;

//初始化栈
//1. 动态分配栈空间
//2. 如果栈空间分配不足,返回NULL
//3. 如果栈空间分配足,就让栈指针默认指向-1并且返回栈空间的地址
seqStack* init_seqStack() {
    seqStack* s = new seqStack;
    
    if(s == NULL) {
        printf("栈空间分配不足");
        return NULL;
    } else {
        s->top = -1;
        return s;
    }
}
//判断栈空,栈空返回1,栈不为空返回0
int empty(seqStack* s) {
    if(s->top == -1)
        return 1;
    
    return 0;
}

//入栈,入栈前要考虑元素是否已满
//1. 判断栈是否已满,如果满了,返回0
//2. 如果栈没满,让栈顶指针往上移,并将元素赋值给指向栈顶指向的位置
//3. 入栈成功返回1, 入栈失败返回0
int push_seqStack(seqStack* s, int x) {
    if(s->top == MAXSIZE - 1) {
        printf("栈已满");
        return 0;
    }
    s->top++;
    s->data[s->top] = x;
    return 1; //入栈成功
    
}

//出栈
//1. 先判断栈内元素是否为空,
// 如果栈里为空,返回0表示出栈失败
//2. 如果栈不为空
// 将栈顶元素值赋值给指针变量x
// 栈顶指针下移
// 返回1表示出栈成功
int pop_seqStack(seqStack* s, int* x) {
    if(empty(s)) {
        printf("栈空, 不能出栈");
        return 0;
    }
    
    *x = s->data[s->top];
    s->top--;
    return 1;
    
}
//获取栈顶元素
//1.先判断栈内元素是否为空,如果为空 return 0 
// 2.如果不为空,返回栈顶的元素
int getTop(seqStack* s) {
    if(empty(s))
        return 0;
        
    return s->data[s->top];
}
int main()
{
    

    return 0;
}

链栈

image.png

#include <iostream>

typedef struct Node {
    int data;
    struct Node* next;
}linkStack;

//初始化链栈
linkStack* init_linkStack() {
    linkStack* top;
    top = NULL;
    return top; //返回栈顶为空指针
}

//判断栈空
int empty(linkStack* top) {
    if(top == NULL)
        return 1;
    
    return 0;
}

//入栈
linkStack* push_linkStack(linkStack* top, int x) {
    linkStack* s;
    s = new linkStack; //动态分配,申请新的栈顶结点空间
    s->data = x;
    s->next = top; //原栈顶结点作为新节点的后继
    top = s;    //将新节点置为栈顶
    return top;
}

//出栈
linkStack* pop_linkStack(linkStack* top, int* x) {
    linkStack* p;
    
    if(top == NULL)
        return NULL; //栈空不能出栈,返回空指针
    
    *x = top->data; //保存栈顶元素值
    p = top;    
    top = top->next; //置新的栈顶指针
    delete p;   //释放原栈顶元素结点空间
    return top; //出栈成功,返回新的栈顶指针
}

共享栈

两个栈的共享技术,也就是双端栈。主要利用了栈底位置不变,而栈顶位置动态变化的特性。首先申请一个共享的一维数组空间S[MaxSIZE]将两个栈的栈底分别放在一维数组的两端,分别是0和 MAXSIZE - 1。由于2个栈顶是动态变化的,所以各个栈可用的最大空间与实际使用的需求有关。

#define MAXSIZE 100

typedef struct {
    int stack[MAXSIZE];
    int top[2]; //两个栈顶指针
}DqStack;

//双端栈初始化
//1.初始化2个栈均为空栈
//2. s是指向栈类型的指针
void init_DqStack(DqStack* s) {
    s->top[0] = -1;
    s->top[1] = MAXSIZE;
}

//双端栈入栈, s是指向栈类型的指针,x是要入的值,k是栈的号数
// 1. 2个栈入栈前,都要判断栈是否已满
// 2.第一个栈入栈,因为第一个栈的栈顶为-1,所以让栈顶指针上移,再进行赋值操作
// 3. 第二个栈入栈,因为第二个栈的栈顶为MAXSIZE,所以让栈顶指针下移,再进行赋值操作
int push_stack(DqStack* s, int x, int k) {
    //s->top[0] == MAXSIZE - 1
    if(s->top[0] == s->top[1] - 1) {
        printf("两个栈均满");
        return 0;
    } 
    
    if(k == 0) {
        s->top[k]++;    
    } else {
        s->top[k]--;
    }
    
    s->stack[s->top[k]] = x;
}

//双端栈出栈
//栈顶元素由参数返回
//1. 出栈前,先判断栈是否为空, 如果为空 return 0 
// 2.取出栈顶元素值给x
// 3. 如果是第一个栈出栈,指针下移; 如果是第二个栈,则指针上移
// 4. 返回1表示出栈成功
int pop_stack(DqStack* s, int* x, int k) {
    if((s->top[0] == -1 && k == 0)|| (s->top[1] == MAXSIZE && k == 1)) {
        printf("栈为空");
        return 0;
    }
    
    *x = s->stack[s->top[k]];
    
    if(k == 0)
        s->top[k]--;
    else
        s->top[k]++;
    
    return 1;
}

栈与递归

什么是递归?

递归是指在一个函数、过程或数据结构定义的内部,直接或间接出现定义本身的应用。 递归的内部实现是通过一个工作栈来保存调用过程中的参数、局部变量和返回地址。

递归函数

一种直接或间接地调用自身函数的过程。

阶乘函数

int fact(int n) {
    if(n == 0)
        return 1;
    else 
        return (n * fact(n - 1));
}

当一个函数在运行期间调用另一个函数时,在运行被调用函数之前,系统需要完成以下3件事

  1. 将所有的实参,返回地址等信息传递给被调用函数保存。
  2. 为被调用函数的局部变量分配存储区。
  3. 将控制转移到被调函数的入口。

而从被调用函数返回函数之前,系统也完成三件事

  1. 保存被调用函数的计算结果。
  2. 释放被调用函数的数据区。
  3. 根据被调用函数保存的返回地址,将控制转移到调用函数。

image.png

image.png

以下三种情况用到递归

  1. 递归定义的数学函数

image.png

  1. 具有递归特性的数据结构

image.png

  1. 可递归求解的问题

image.png

递归问题-> 用分治法求解

image.png

image.png

image.png

当多个函数嵌套调用时

遵循最后调用的函数是最先返回的 image.png

image.png

递归的优缺点

image.png

栈的应用

子程序的嵌套调用、操作系统的中断处理,在程序的编译和运行过程中,还要利用栈堆程序的语法进行检查,如括号的匹配、表达式的求值和函数的递归调用等。 当应用程序中需要使用与数据存储顺序相反的数据时,就要想到使用栈。

数制转换问题

将十进制数N转换为d进制数,利用辗转相除法。 N = (N div d) * d + N mod d (div整除运算,mod求与余运算)

  1. 定义一个栈变量s
  2. 定义一个整型变量x, 用来接收弹栈的元素
  3. 初始化栈
  4. 循环 条件(N)->一直循环 操作:入栈(s, N % d), 往栈s里入余数; N = N / d;(N一直整除d)
  5. 循环 条件(栈不为空) 操作:弹栈(s, &x)
  6. 输出x
void conversion(int N, int d) {
    seqStack* s;
    int x;
    
    init_seqStack(&s);
    
    while(N) {
        push_seqStack(&s, N % d);
        N = N / d;
    }
    while(!empty(s)) {
        pop_seqStack(s, &x);
        printf("%d ", x);
    }
}

利用数组实现数制转换

  1. 定义一个栈数组,一个指针变量top(栈顶)
  2. 定义一个变量x,用来接收弹栈的余数
  3. top默认等于-1,表示空栈
  4. 循环 条件(N) 操作: 栈顶指针上移; 将余数入栈;N整除d
  5. 循环 条件(栈不为空) 操作:将栈顶的元素(余数)赋值给x; 栈顶指针下移; 输出x
void conversion(int N, int d) {
    int stack[MAX], top;
    int x;
    top = -1;
    
    //入栈与我们需要的操作的数据是截然相反的结果
    while(N) {
        top++;
        stack[top] = N % d; //余数入栈
        N = N / d;
    }
    //只要栈顶指针不指向-1,即栈不为空
    while(top != -1) {
        x = stack[top]; //余数按顺序出栈
        top--;
        printf("%d ", x);
    }
    
}

利用链表实现数制转换

void conversion(int N, int d) {
    linkStack* top; //栈顶指针
    linkStack* p;
    
    top = NULL; //栈顶指针默认为NULL
    
    while(N) {
        p = new linkStack; //申请新的栈顶结点空间
        p->data = N % d;
        p->next = top;
        top = p;
        N = N / d;
    }
    
    while(top != NULL) {
        p = top;
        printf("%d", p->data);
        top = top->next;
        delete p;
    }
}

括号匹配检验

思路:

#include <iostream>
#include <string.h>

char pairs(char a) {
    if(a == ')') return '(';
    if(a == ']') return '[';
    if(a == '}') return '{';
    return 0;
}

bool isValid(char* s) {
    int n = strlen(s);
    if(n % 2 == 1) {
        return false;
    }
    
    int stack[n + 1], top = 0;
    
    //遍历字符串s
    for(int i = 0; i < n; i++) {
        char ch = pairs(s[i]);
        
        if(ch) {
            if(top == 0 || stack[top - 1] != ch) {
                return false;
            }
            top--;
        } else {
            stack[top++] = s[i];
        }
        
    }
    return top == 0;
}

int main()
{
    char s[] = "(()()())";
   printf("%d", isValid(s));

    return 0;
}

队列

队列具有先进先出的特点,限定元素的操作分别在表的两端进行,通常表尾进行插入操作,表头进行删除操作。

队列的主要操作是入队和出队。顺序表示的循环队列的入队和出队操作,要注意判断队满或队空。凡是涉及队头或队尾指针的修改,都要将其对MAXSIZE求模。

链队列

#include <iostream>

typedef int ElemType;

typedef struct Node {
    ElemType data;
    struct Node* next;
}QNode, *QueueP;

typedef struct {
    QueueP front, rear;
}LinkQueue;


LinkQueue* initQueue() {
    LinkQueue* Q; //申请头尾指针节点
    Q = new LinkQueue;
    Q->front = Q->rear = new QNode;
    
    if(Q->front != NULL) {
        Q->front->next = NULL;
        return Q;
    } else {
        return 0;
    }
}
//链队列入队
void enqueue(LinkQueue* Q, ElemType x) {
    QNode* p;
    p = new QNode; //申请新节点
    
    p->data = x;
    p->next = NULL;
    Q->rear->next = p; //将新节点插入队尾
    Q->rear = p; //队尾指针指向新的节点
}
//判队空
//成功返回1, 失败返回0
int empty(LinkQueue* Q) {
    if(Q->front == Q->rear) //队头队尾指向同一节点,队列为空
        return 0;
    else
        return 1;
}

//链队列出队
int dequeue(LinkQueue* Q, ElemType* x) {
    QNode* p;
    //出队操作首先判断队列Q是否为空
    if(empty(Q)) {
        printf("队列为空\n");
        return 0;
    //否则,取出Q的队头元素,用x返回其值,修改头指针
    } else {
        p = Q->front->next;
        Q->front->next = p->next;
        *x = p->data;
        delete p;
        
        //当队列中最后一个元素被删除后,将队尾指针重复赋值为头结点
        if(Q->front->next == NULL) 
            Q->rear = Q->front;
        
        return 1;
    }
}

循环队列

在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队列到队尾的元素外,也需设2个整形变量front和rear,分别指示队列队头元素和队尾元素的位置。

在队列的顺序存储结构中,初始化空队列时,令front=rear=0,每当插入新的队尾元素时,尾指针rear增1,;每当删除队头元素时,头指针front增1.所以,在非空队列中,头指针始终指向队列头元素,而尾指针始终指向队尾元素的下一个位置。

随着入队、出队的进行,会使整个队列整体向后移动,当队尾指针移到了最后,再有元素进入,就会出现溢出。此时,队列中并未真的占满,这种现象称为“假溢出”。 这是由于“队尾入,队首出”这种受限制的操作所造成的,真正队满的条件应是 rear - front == MAXSIZE

如何解决假溢出问题。通过循环队列这种方式。随着入队、出队的进行,此时,“队满”和“队空”的条件是相同的。

解决办法1:增设一个存储队列中元素个数的变量(count),当count=0时,队空。 当count = MAXSIZE时,队满

解决办法2:少用一个元素空间,当队列空间大小为MAXSZIE时,有MAXSIZE的元素就认为队满,队尾指针永远追不上头指针。

判断队满的条件:(rear + 1) % MAXSIZE = front

判队空的条件:front == rear

由循环队列的存储特性可知,如果应用程序中使用循环队列,则必须申请一个合理的队列长度;若无法预估所用队列的大小,则宜采用链队列。

#include <iostream>

#define MAXSIZE 100 //顺序队列可达到的最大长度
typedef int ElemType;

//循环队列
typedef struct {
    ElemType data[MAXSIZE];
    int front;
    int rear;
} seQueue;

//循环队列的初始化
seQueue* initQueue() {
    seQueue* Q;
    Q = new seQueue;
    Q->front = Q->rear = 0;
    return Q;
}

//入队
// 1.先判断队列是否满,若满则出错
// 2.否则将新元素插入队尾,队尾指针加1
int enqueue(seQueue* Q, ElemType x) {
    if((Q->rear + 1) % MAXSIZE == Q->front) {
        printf("队列已满");
        return 0;
    } else {
        Q->data[Q->rear] = x;
        Q->rear = (Q->rear + 1) % MAXSIZE; //队尾指针增1
        return 1;
    }
}
//出队
//1.先判断队列是否为空,若空则报错
// 2.否则队头元素出队,队头指针增1
int dequeue(seQueue* Q, ElemType* x) {
    if(Q->front == Q->rear) {
        printf("队空");
        return 0;
    } else {
        *x = Q->data[Q->front]; //将队头元素存储在变量x上
        Q->front = (Q->front + 1) % MAXSIZE;
        return 1;
    }
}

队列的应用

许多应用程序需要处理排队等候问题。如售票系统、机场管理系统等。凡是符合先进先出原则的数学模型,都可以用队列来实现。

串是由0个或任意多个字符组成的有限序列, 串是一种其数据元素固定为字符的线性表,串上的操作主要是针对串的整体或串的一部分子串进行的。

串的相关术语

  1. 空串: 不含任何字符的串, 即串的长度 n = 0, 记为"".
  2. 空格串:由一个或多个称为空格的特殊字符组成的串,其长度是串中空格字符的个数。
  3. 主串:包含子串的串
  4. 模式匹配:子串的定位运算又称为串的模式匹配, 求子串在主串中第一次出现的第一个字符的位置
  5. 两个串相等: 两个串的长度相等且各个位置上对应的字符也都相同。

串的应用

1.在汇编和高级语言的编译程序中,源程序和目标程序都以字符串来表示。 2.在事务处理程序中,如客户的姓名、地址、邮政编码和货物名称等都是用字符串数据处理的。 3. 信息检索系统、文字编辑系统和语言翻译系统等。

串的基本操作

  1. strAsign(S, chars) //串的赋值 初始条件:chars是字符串常量 操作结果:生成一个值等于chars的串S

  2. strCopy(S, T) //串的复制

  3. strLength(S) //求串的长度 1). 初始化一个变量i为0,用于统计串的长度 2). 循环:条件(串的字符数组元素不等于'\0') 操作:i++; 循环结束后,s的长度等于i 3). 返回串的长度

  4. strCat(S, T) //串的连接

  5. subString(sub, S, pos, len) //求子串 初始条件:串S存在 , 1 <= pos <= strLength(S) 且 1 <= len <= strLength(S) - pos+1

  6. strIndex(S, T) //串的定位,模式匹配

  7. strInsert(S, pos, T) //串的插入

  8. strDelete(S, pos, len) //串的删除

  9. strReplace(S, T, V) //串的替换

  10. strEmpty(S) //判断栈空

  11. strCompare(S, T) //串的比较

  12. strClear(S) //串的清空

  13. printStr(S) //输出串

  14. createStr(S) //建立串

  1. 调用求串长度子函数
  2. 将strLength(S)函数值赋值给S->Len

image.png

image.png

串的存储结构

顺序存储

  1. 顺序串可以用一个字符型数组和一个整型变量来表示,其中,字符数组存储串,整型变量表示串的长度。 例如 串 S = "Beijing", 字符串从S.ch[0]单元开始存放,用'\0'来表示串的结束

image.png

  1. 存储方式 当计算机按字节为单位编址时,一个存储单元刚好存放一个字符,串中相邻的字符按顺序存储在地址相邻的存储单元中。 当计算机按字(1个字为32位)为单元编址时,一个存储单元由4个字节组成。

1)非紧凑存储:计算机字长为32位(4个字节), 用非紧凑格式,一个地址只能存放一个字符, 其优点是运算处理简单,但缺点是存储空间十分浪费。 2) 紧凑存储:一个地址能存4个字符, 优点是空间利用率高,缺点是对串中字符的处理效率较低。

#include <iostream>
#define MAXSIZE 100 //顺序串存储空间的总分配量

typedef struct {
    char ch[MAXSIZE]; //存储串的字符数组
    int Len;    //存储串的长度
}String;    //串结构定义

//求串的长度
int strLen(String* s) {
    //i用来统计字符串的长度
    int i = 0;
    
    while(s->ch[i] != '\0') 
        i++;
    
    s->Len = i;
    return s->Len;
}

//建立串的算法
void createStr(String* s) {
    //建立一个新串函数
    fgets(s->ch, strLen(s), stdin);
    s->Len = strLen(s);
}

//求子串
//求串S从第pos位置开始,长度为len的子串,并将其存入到串sub中
//操作成功,返回1;操作失败,返回0
int subString(String* s, String* sub, int pos, int len) {
    int j;
    //判断边界条件
    //如果 pos小于1或pos大于字符串的长度或 len(子串的长度)小于1
    if(pos < 1 || pos > s->Len || len < 1 || len > s->Len - pos + 1) {
        sub->Len = 0;
        printf("参数错误\n");
        return 0;
    } else {
        for(j = 0; j < len; j++) {
            sub->ch[j] = s->ch[pos + 1];
            sub->ch[j] = '\0';
            sub->Len = len;
            return 1;
        }
    }
}
//删除子串
//将删除串s从指定位置i开始的连续每个字符

int strDelete(String* s, int i, int l) {
    int k;
    
    //判断删除位置和删除串长度是否出错
    if(i + l - 1 > s->Len) {
        printf("所要删除的子串越界");
        return 0;
    } else {
        for(k = i + l -1; k < s->Len; k++, i++) {
            s->ch[i - 1] = s->ch[k];
            s->Len = s->Len - l;
            s->ch[s->Len] = '\0';
            return 1;
        }
    }
}

//插入子串的算法
int strInsert(String* s, String* s1, int i) {
    int k;
    if(i > s->Len + 1) {
        printf("插入位置出错");
        return 0;
    } else if(s->Len + s1->Len > MAXSIZE) {
        printf("两串长度超过存储空间长度!");
        return 0;
    } else {
        
        for(k = s->Len - 1; k >= i; k--) 
            s->ch[s1->Len + k] = s->ch[k];
            
        for(k = 0; k < s1->Len; k++)
            s->ch[i + k - 1] = s1->ch[k];
        
        s->ch[i + k - 1] = s1->ch[k];
        
        s->Len = s->Len + s1->Len;
        
        s->ch[s->Len] = '\0';
        
        return 1;
    }
}

//串定位
int strIndex(String* s, String* t) {
    int i = 0, j = 0, k;
    while(i < s->Len && j < t->Len) {
        
        if(s->ch[i] == t->ch[i]) {
            i++;
            j++;
        } else {
            i = i - j + 1;
            j = 0;
        }
    }
    
    if(j >= t->Len)
        k = i - t->Len + 1;
    else
        k = 1;
        
    return k;
}

//串相等
int strCompare(String* s1, String* s2) {
    int i = 0, flag = 0;
    
    //当两个串没到字符串尾部时
    while(s1->ch[i] != '\0' && s2->ch[i] != '\0') {
        if(s1->ch[i] != s2->ch[i]) {
            flag = 1;
            break;
        } else
            i++;
    }
    
    if(flag == 0 && s1->Len == s2->Len) 
        return 0;
    else
        return s1->ch[i] - s2->ch[i];
}

//串连接
int strCat(String* s, String* t) {
    int i, flag;
    if(s->Len + t->Len <= MAXSIZE) {
        s->ch[i] = t->ch[i - s->Len];
        s->ch[i] = '\0';
        s->Len += t->Len;
        flag = 1;
    } else if(s->Len < MAXSIZE) {
        
        for(i = s->Len; i < MAXSIZE; i++) 
            s->ch[i] = t->ch[i - s->Len];
            
            s->Len = MAXSIZE;
            flag = 0;
       
    }  else 
            flag = 0;
        
        return flag;
}

//子串替换算法
void strReplace(String* s, String* t, String* v) {
    int i, m, n, p, q;
    n = s->Len;
    m = t->Len;
    q = v->Len;
    p = 1;
    
    do {
        i = strIndex(s, t);
        if(i != -1) {
            strDelete(s, i, m);
            strInsert(s, v, i);
            p = i + q;
            s->Len = s->Len + q - m;
            n = s->Len;
        }
    }while((p <= n - m + 1) && (i != -1));
}

int main()
{
    
    return 0;
}

链式存储

用链表存储字符串,每个结点有2个域:一个数据域(data)和一个指针域(next) data——用于存放字符串中的字符

next——用于存放后继结点的地址

优点:插入、删除运算方便

缺点:存储、检索效率低

在各种字符串的处理系统中,所处理的字符串往往又长又多。这时就必须考虑字符串的存储密度。

存储密度 = 字符串所占的存储位 / 实际分配的存储位

字符串链式存储的存储密度小,存储空间比较浪费,因此不太实用。

堆存储

在实际应用中,往往要定义很多字符串,并且各个字符串长度在定义之前又无法确定,在这种情况下,可以采用堆存储(索引存储),这是一种动态存储结构

堆存储的方法

  1. 开辟一块地址连续的存储空间,用于存储各字符串的值,该存储空间称为“堆”(自由存储区)。
  2. 另外建一个索引表,用来存储字符串的名字(name)、长度(length)和该串在“堆”中的起始地址。
  3. 程序执行过程中,每产生一个串,系统就会从“堆”中分配一块大小与串的长度相同的连续空间,用于存储该串的值,并且在索引表中增加一个索引项,用于登记该串的名字、长度和串的起始地址。
typedef struct {
    char name[MAXLEN]; //串名
    int length; //串长
    char* start;    //起始地址
}LNode;

串的模式匹配

子串的定位操作通常称作串的模式匹配(其中T被称为模式串),确定主串中所含子串(模式串)第一次出现的位置(定位)。是各种处理系统中最重要的操作之一。

例如:在一篇文章中查找某个关键字,把文章看作主串,关键字看作子串。

算法应用

  1. 搜索引擎
  2. 拼写检查
  3. 语言翻译
  4. 数据压缩

算法种类

  1. BF算法

image.png

image.png

image.png

image.png

image.png

image.png

  1. KMP算法(特别快)

image.png

数组和广义表

数组的定义

数组是由n个类型相同的数据元素组成的有限序列。其中,这n个数据元素占用一块地址连续的内存空间。数组中的数据元素,可以是原子类型(int、float、char等)的,也可以是一个线性表,这种类型的数组称为二维数组。

所以,一个n维数组可以看成是若干个n-1维数组组成的线性表。

二维数组可以看做是一个定长线性表,它的每一个元素也是一个定长线性表。可以用一个矩阵来表示一个二维数组。先行再列

image.png

image.png

image.png

image.png

image.png

计算数组的地址

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

数组的基本操作

由于数组一经生成,其数据元素的个数就固定了,元素在内存中的存储空间也固定了,因此不能往数组中插入新元素,也不能删除数组中的元素。

init_Array(A, n, d[]) //生成一个n维的数组A,其中d[]中存放各维数的下标值
Destroy_Array(A) /销毁一个n维数组A并释放器占用的内存空间
getValue(A, idx[], val) //从A中读取某个元素的值,存入变量val中,并将钙元素的各维下标值入idx[]
setValue(idx[], v, A) //将变量v的值赋值给n维数组A中某个元素,该元素的位置由idx[]下标值来确定。

树和二叉树

树的定义

树是n(n >= 0)个结点的有限集合。 当n = 0时,则这棵树为空树。 在一颗非空树中:

  1. 有且仅有一个根节点, 根结点无直接前驱,但有0个或多个直接后继
  2. 除根节点外的其余结点可被分成m(m >= 0)个互不相交的有限集合,每个集合本身又是一棵树,称为根节点的子树。每个子树的根节点有且仅有一个直接前驱,但有0个或多个直接后继。

image.png

image.png 树的深度(高度)为4

image.png

image.png

image.png

树可以描述为二元组的形式 T = (D, R) D为树中结点的集合,R为树中结点之间关系的集合 D = {a, b, c, d, e, f} R = {(a, b), (a, c), (a, d), (c, e), (c, f)}

image.png

image.png

image.png

image.png

遍历二叉树

是指从根结点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问依次且仅被访问一次。 四种遍历方式分别为:先序遍历、中序遍历、后序遍历、层序遍历。

先序遍历 (根 左 右)

  1. 若二叉树为空,则为空操作
  2. 访问根节点
  3. 先序遍历节点的左子树
  4. 先序遍历节点的右子树

image.png

#include <iostream>

typedef int ElemType;

typedef struct BitNode {
	ElemType data; //数据域 
	struct BitNode *lChild, *rChild;	//左右孩子指针 
}BitNode, *BitTree;


void preOrder(BitTree bt) {
	if(bt != NULL) {
		printf("%d", bt->data); //根
		preOrder(bt->lChild); //左
		preOrder(bt->rChild); //右 
	}
}

非递归算法

//根左右
void preOrder(Node* root) {
    stack<Node*> s;
    Node* cur = root;
    
    while(true) {
            while(cur != NULL) {
            cout << cur->data << " ";
            s.push(cur);
            cur = cur->left;
        }
        
        if(s.empty()) 
            return;
        cur = s.top();
        s.pop();
        cur = cur->right;
        }
}

中序遍历 (左 根 右)

  1. 若二叉树为空,则为空操作
  2. 中序遍历根节点的左子树
  3. 访问根节点
  4. 中序遍历根节点的右子树

image.png

void inOrder(BitTree bt) {
	if(bt != NULL) {
		inOrder(bt->lChild);
		printf("%d", bt->data);
		inOrder(bt->rChild);
	}
}

后序遍历 (左 右 根)

  1. 若二叉树为空,则为空操作
  2. 后序遍历根节点的左子树
  3. 后序遍历根节点的右子树
  4. 访问根节点

image.png

递归写法

void postOrder(BitTree bt) {
	if(bt != NULL) {
		postOrder(bt->lChild);
		postOrder(bt->rChild);
		printf("%d", bt->data);
	}
}

非递归写法

#include<bits/stdc++.h>

using namespace std;

typedef struct Node {
    int data; //数据域
    struct Node* left; //指向左子树的指针
    struct Node* right; //指向右子树的指针
    
    //初始化树的结点
    Node(int data) {
        this->data = data;
        left = right = NULL;
    }
}Node;

/*
            1           stack->1  
          /   \         stack->2->1
        2      3        stack->4->2->1
      /  \
    4     5 
*/
//中序遍历
void inOrder(Node* root) {
    //通过栈的方式实现树的遍历
    stack<Node*> s;
    Node* cur = root; //让指针cur指向根节点
    
    //只要指针cur不指向空 或 栈不为空
    while(cur != NULL || s.empty() == false) {
        
        //只要cur不为空,就将根节点开始入栈,然后遍历左子树
        // 并将其值依次入栈
        while(cur != NULL) {
            s.push(cur);
            cur = cur->left;
        }
        //cur为空时,就让它指向栈顶
        cur = s.top();
        //依次从栈顶弹出值
        s.pop();
        
        cout << cur->data << " ";
        
        cur = cur->right;
    }
} 

int main()
{
    Node* root = new Node(1);
    root->left = new Node(2);
    root->right = new Node(3);
    root->left->left = new Node(4);
    root->left->right = new Node(5);
    
    inOrder(root);
    return 0;
}

层序遍历

层序遍历嘛,就是按层,从上到下,从左到右遍历,这个没啥好说的。

image.png

搜索/查询

顺序查找

#include <iostream>

using namespace std;

int linearSearch(int arr[], int length, int value) {
    //遍历数组, 逐个比较,如果数组中的值等于value,返回要查找值的下标,否则返回-1
    for(int i = 0; i < length; i++) {
        if(arr[i] == value)
            return i; 
    }
     return -1;
}

int main()
{
    int arr[] = {1, 4, 0, 2, 3, 8, 10, 22, 90, 6};
    int length = sizeof(arr) / sizeof(arr[0]);
    int value = 10;
    
    int result = linearSearch(arr, length, value);
    
    (result == -1) ? cout << "查找失败" : cout << "查找成功, 该元素的下标为 " << result;
    return 0;
}

二分查找

#include <iostream>

using namespace std;

//二分查找------只能对有序的序列进行查找
int binarySearch(int arr[], int length, int value) {
    int low = 0;
    int high = length - 1;
    int mid;
    
    while(low <= high) {
          mid = (low + high) / 2;
         if(value == arr[mid]) {
             return mid;
         } else if(value < arr[mid]) {
             high = mid - 1;
         } else 
              low = mid + 1;
    }
    
    return -1;
    
}

int main()
{
    int arr[] = {2, 4, 6, 8, 10, 12, 14, 16};
    int length = sizeof(arr) / sizeof(arr[0]);
    int value = 10;
    
    int result = binarySearch(arr, length, value);
    
    (result == -1) ? cout << "查找失败" : cout << "查找成功, 该元素的下标为 " << result;
    return 0;
}

搜索二叉树

#include <iostream> 

using namespace std;


/**
 *  若左子树和右子树不为空,则搜索二叉树所有左子树上的结点值都小于根节点的值,所有右子树上的结点值都大于根节点的值
 *  搜索左子树和右子树同样满足二叉搜索树的性质。
 */ 

//搜索二叉树
typedef struct Node {
  int data;
  struct Node* lchild;
  struct Node* rchild;
  
  Node(): data(0), lchild(nullptr), rchild(nullptr) {}
  Node(int value): data(value), lchild(nullptr), rchild(nullptr) {}
} Node, *BSTree;


//从二叉搜索树中查询某个值
BSTree search(BSTree root, int data) {
    //如果根节点为空或 要查找的值等于根节点的值  就返回根节点
    if(root == nullptr || root->data == data) 
        return root;
        
    //如果要查找的值大于根节点的值,就从右子树上找
   if(root->data < data) 
        return search(root->rchild, data);
    //如果要查找的值小于根节点的值,就从左子树上找
     else 
        return search(root->lchild, data);
}
//插入
BSTree insert(BSTree &root, int data) {
    //动态创建一个新的节点
    BSTree newNode = new Node;
    //如果根节点为空,就插入一个新的节点
    if(!root) {
        newNode->data = data;
        newNode->lchild = nullptr;
        newNode->rchild = nullptr;
        return newNode;
    }
    //如果要插入的值大于根节点的值,就从右子树上插入
    if(root->data < data)
        root->rchild = insert(root->rchild, data);
    //如果要插入的值小于根节点的值,就从左子树上插入
    else 
        root->lchild = insert(root->lchild, data);
        
    return root;
}

//中序遍历二叉树,得到的是一个有序的序列
void inorder(BSTree root) {
    //如果根节点为空,就退出
    if(!root) 
        return;
    
    //中序:左根右
    inorder(root->lchild);
    
    cout << root->data << " ";
    
    inorder(root->rchild);
}
//找最小值
BSTree minValueNode(BSTree root) {
    BSTree cur = root; //cur指向根结点
    
    //一直往左遍历, 直到cur和cur的左子树为空
    while(cur && cur->lchild) 
        cur = cur->lchild;
    
    return cur;
}


//删除节点
/**
 *  1. 叶子结点直接删 
 *  2. 单子结点(左子或右子),子结点顶替被删除的结点
 *  3. 双子结点:从右子树种找到最小值(min)的结点或从左子树中找到最大值(max)的结点,顶替被删除的结点
 */
 BSTree deleteNode(BSTree root, int data) {
     //判空
     if(root == nullptr)
        return root;
    
    //如果要删除的结点值大于根节点的值,就从右子树开始找
    if(data > root->data) {
        root->rchild = deleteNode(root->rchild, data);
    //如果要删除的结点值小于根节点的值,就从左子树开始找
    } else if(data < root->data) {
        root->lchild = deleteNode(root->lchild, data);
    } else {
        //如果要删除的结点值等于根结点的值,就删除
        //叶子结点, 直接删
        if(root->lchild == nullptr && root->rchild == nullptr)
            return nullptr;
            // free(root);
        
        //双子结点
        else if(root->rchild && root->lchild) {
            BSTree temp = minValueNode(root->rchild); //从右子树中找出最小值的结点
            root->data = temp->data;
            root->rchild = deleteNode(root->rchild, temp->data);
        }
        else {
        //单子结点
           if(root->lchild == nullptr) {
               BSTree temp = root->rchild;
               root->data = temp->data;
               delete root;
			   return temp;
           } else {
               BSTree temp = root->lchild;
               root->data = temp->data;
               delete root;
			   return temp;
           }
        }
    }
    
    return root;
 }

int main()
{
    BSTree root = nullptr;
    // Node no;
    root = insert(root, 50);
    root = insert(root, 30);
    root = insert(root, 70);
    root = insert(root, 20);
    root = insert(root, 40);
    root = insert(root, 60);
    root = insert(root, 80);
    inorder(root);
    
    cout << "\n---------------" << endl;
    
    deleteNode(root, 20);
    // inorder(root);
    //删除30
    deleteNode(root, 30);
    inorder(root);
    
    return 0;
}


散列表

排序

插入排序

将数组分成一个排序好和未排序好的元素序列,从未分类元素序列中挑选值并放置到排序序列的正确位置。

思路:

  1. 按升序对大小为n的数组进行排序, 即从arr[1]到arr[n]在数组中重新排列
  2. 将当前元素(i),记录到临时变量tmp中,与其前身(j)进行比较
  3. 如果关键元素比前身元素小,则将其与之前的元素进行比较。将较大的元素向上移动一个位置,为交换的元素腾出空间。

image.png

直接插入排序

void insertSort(int arr[], int len) {
    //检查数据合法性
    if(arr == nullptr || len <= 0)
        return;
    
    for(int i = 1; i < len; i++) {
        int tmp = arr[i];
        int j;
        
        for(j = i - 1; j >= 0; j--) {
            //如果比tmp大,就把值往后移动一位
            if(arr[j] > tmp) {
                arr[j + 1] = arr[j];
            } else {
                break;
            }
        }
        //插入到正确位置
        arr[j + 1] = tmp; 
    }
}
void InsertSort(int arr[], int len) {
     //检查数据合法性
    if(arr == nullptr || len <= 0)
        return;

    int key, i, j;
    
    for(i = 1; i < len; i++) {
        key = arr[i];    //记录当前元素
        //j用来记录前身元素
        j = i - 1;
        
        // j >= 0 表示不能越界 当前元素比前身元素小,就让前身元素往后移,腾位置
        while(j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        //当循环结束后,表示arr[j] < key, 即前身元素小于当前元素,那我们就将当前元素插入到前身元素后边
        arr[j + 1] = key;
    }
}

折半插入排序

可以用来减少直接插入排序中的比较次数,该排序使用二分查找在每个循环中插入所选值的正确位置。

void BinaryInsertSort(int arr[], int len) {
     //检查数据合法性
     if(arr == nullptr || len < 0)
        return;
    
    int key, low, high;
    
    for(int i = 1; i < len; i++) {
        key = arr[i];
        low = 0; 
        high = i - 1; //low~high是已排好序的元素序列
        
        //二分查找找出第一个比key大的数
        while(low <= high) {
            int mid = (low + high) / 2;
            
            if(arr[mid] > key)
                high = mid - 1;
            else 
                low = mid + 1;
        }  //循环结束,high+1则为插入位置
        
        //将元素依次后移,直到high+1这个位置也移动到后边去
        for(int j = i - 1; j >= low; j--) {
            arr[j + 1] = arr[j];    
        }
        // arr[low] = key;
        arr[high + 1] = key;
    }
}

希尔排序

void shellSort2(int arr[], int n) {
   for(int gap = n / 2; gap > 0; gap /= 2) {
       for(int i = gap; i < n; i++) {
               int temp = arr[i];
               int j = i;
                
               
               while(j >= gap && arr[j - gap] > temp) {
                   arr[j] = arr[j - gap];
                   j -= gap;
               }
               
               arr[j] = temp;
             }
      }
}

快速排序

#include <iostream>

using namespace std;

int partition(int arr[], int low, int high) {
	int pivot = arr[low]; //将第一个数作为中心点
	while(low < high && arr[high] >= pivot) {
		high--;
	} 
	arr[low] = arr[high];
	
	while(low < high && arr[low] <= pivot) {
		low++;
	}
	arr[high] = arr[low];
	
	arr[low] = pivot;
	
	return low;
}

void quickSort(int arr[], int low, int high) {
	if(low < high) {
		int part = partition(arr, low, high);
		//左半部分
		quickSort(arr, low, part - 1);
		//右半部分 
		quickSort(arr, part + 1, high);
	}
}

void PrintArray(int arr[], int len) {
	for(int i = 0; i < len; i++) {
		cout << arr[i] << " ";
	}
	cout << endl;
}

int main() {
	int arr[] = {20, 1, 5, 6, 2, 4, 7};
	int len = sizeof(arr) / sizeof(arr[0]);
	quickSort(arr, 0, len - 1);
	PrintArray(arr, len);
	
	return 0;	
}

选择排序

时间复杂度 O(n^2)

特点:

  1. 由于算法过程中可能改变相同关键字的前后顺序,可能产生不稳定现象。
  2. 可用于链式存储结构
  3. 移动记录次数少,当每个记录占用的空间较多时,其排序效率较高

原理: 找到数组中的最小值并将其放在第一个位置。在那之后,它找到第二个最小的值并把它放到第二个位置重复这个过程,直到整个数组排序完毕

伪代码:

  1. for i = 1 to (n - 1)
  2. min = i //设当前元素为最小值
  3. for j = i + 1 to n
  4. if list[j] < list[min] then min = j; end if end for if indexMin != i then swap list[min] and list[i] end if end for
#include <iostream>

void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

void selectionSort(int arr[], int n) {
    int i, j, min;
    
    for(i = 0; i < n - 1; i++) {
        min = i; //假设当前元素是最小的元素
        
        //从unsorted array找到最小值
        for(j = i + 1; j < n; j++) {
            if(arr[j] < arr[min]) {
                    min = j;
            }
        }
        swap(&arr[min], &arr[i]);
    }
}

int main()
{
    int arr[5] = {3, 5, 1, 4, 2};
    int n = sizeof(arr) / sizeof(arr[0]);
    selectionSort(arr, n);
    
    printf("\n排序后,数组为:");
    for(int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    
    return 0;
}

堆数据结构

堆的常用方法:

  • 构建优先队列
  • 支持堆排序
  • 快速找出一个集合中的最小值(或者最大值)

堆数据结构是一个完整的二叉树,满足堆属性,其中任何给定的节点是 最大堆:根节点的值是所有节点中最大的 最小堆:根节点的值是所有节点中最小的

image.png

image.png

#include <iostream>

using namespace std;

int n = 0;

void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

//堆的初始化
void heapify(int arr[], int n, int i) {
    if(n == 1) {
        cout << "堆里只有一个元素";
    } else {
        int largest = i;
        int l = 2 * i + 1;
        int r = 2 * i + 2;
        if(l < n && arr[l] > arr[largest])
            largest = l;
        if(r < n && arr[r] > arr[largest])
            largest = r;
        
        if(largest != i) {
            swap(&arr[i], &arr[largest]);
            heapify(arr, n, largest);
        }
    }
}
void insert(int arr[], int val) {
    if(n == 0) {
        arr[0] = val;
        n++;
    } else {
        arr[n] = val;
        n++;
        
        for(int i = n / 2 - 1; i >= 0; i--) {
            heapify(arr, n, i);
        }
    }
}
void deleteNode(int arr[], int num) {
    int i;
    for(i = 0; i < n; i++) {
        if(num == arr[i]) 
            break;
      
    }
     swap(&arr[i], &arr[n - 1]);
      n--;     
          
    for(int i = n / 2 - 1; i >= 0; i--) {
            heapify(arr, n, i);
    }
   
    
}
void printArray(int arr[], int n) {
    for(int i = 0; i < n; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;
}


int main() {
    int arr[8];
    
    insert(arr, 3);
    insert(arr, 4);
    insert(arr, 9);
    insert(arr, 5);
    insert(arr, 2);
    // int n = sizeof(arr) / sizeof(arr[0]);
    
    cout << "Max-Heap Array:";
    printArray(arr, n);
    
    deleteNode(arr, 4);
    cout << "\n删除4之后";
    printArray(arr, n);
    return 0;
}

堆排序

#include <iostream>

using namespace std;

//最大堆调整
void heapify(int arr[], int n, int i) {
    //找到最大结点值
    int largest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    
    if(left < n && arr[left] > arr[largest])
        largest = left;
    if(right < n && arr[right] > arr[largest])
        largest = right;
    
    //如果根结点值不是最大的,就交换
    if(largest != i) {
        swap(arr[i], arr[largest]);
        //第一次调整后,其他子树并没有满足堆的特性
        //所以还要再递归一次, 直到largest == i
        heapify(arr, n, largest);
    }
}

//Build-Max-Heap 将自下而上的调用 Max-Heapify 来改造数组,建立最大堆
void heapSort(int arr[], int n) {
    //建立最大堆
    for(int i = n / 2 - 1; i >= 0; i--) 
        heapify(arr, n, i);
    
    for(int i = n - 1; i >= 0; i--) {
        swap(arr[0], arr[i]);
        
        heapify(arr, i, 0);
    }
}
void printArray(int arr[], int n) {
    for (int i = 0; i < n; ++i)
      cout << arr[i] << " ";
    cout << "\n";
 }
  
int main()
{
    int arr[] = {1, 12, 9, 5, 6, 10};
    int n = sizeof(arr) / sizeof(arr[0]);
    heapSort(arr, n);
    
    cout << "Sorted array is \n";
    printArray(arr, n);
    return 0;
}

归并排序

  1. 给一个未排序的数组,怎么实现排序呢
  2. 先拆后合, 先找到中间值,然后一分为二,拆成左边和右边分别递归调用mergeSort()
  3. 最后合并, 调用merge(), 把2段合并好的数合并成一个大的排序好的数
void merge(int arr[], int start, int mid, int end) {
    int n1 = mid - start + 1;
    int n2 = end - mid;
    int L[n1], R[n2];
     
     for(int i = 0; i < n1; i++) {
         L[i] = arr[start + i]; //复制数组
     }
     for(int i = 0; i < n2; i++) {
         R[i] = arr[mid + 1 + i]; 
     }
     
     int p1 = 0;  //S1: start~mid  S2: mid+1 ~ end 
     int p2 = 0;
     int p = start; 
     
     //比较两副牌的大小, 从每幅牌的第一张开始比较, 小的放入到新数组中
     while(p1 < n1 && p2 < n2) {
         if(L[p1] <= R[p2]) {
             arr[p] = L[p1]; //小的元素放入到新数组中
             p1++;
            //  p++;
         } else {
             arr[p] = R[p2];
             p2++;
            //  p++;
         }
         //每判断完一次,更新新数组的下标
         p++;
     }
     
     //当左边还有牌时,就将剩余的牌添加到新数组中
     while(p1 < n1) {
         arr[p] = L[p1];
         p1++;
         p++;
     }
     //当右边还有牌时,就将剩余的牌添加到新数组中
     while(p2 < n2) {
         arr[p] = R[p2];
         p2++;
         p++;
     }
}

//归并排序
void mergeSort(int arr[], int start, int end) {
    if(start < end) {
        int mid = start + (end - start) / 2;
        //拆成左边和右边分别递归调用mergeSort
        mergeSort(arr, start, mid);
        mergeSort(arr, mid + 1, end);
        
        //合并
        merge(arr, start, mid, end);    
    }
}

/*
void merge(int arr[], int start, int mid, int end) {
     int n = end - start + 1; //数组长度
     int temp[n]; //临时数组,将2个排好序的数组合并成一个大的排好序的数组
    // int n1 = mid - start + 1;
    // int n2 = end - mid;
     int L[100], R[100];
     
     for(int i = 0; i < mid-start + 1; i++) {
         L[i] = arr[start + i]; //复制数组
     }
     for(int i = 0; i < end - mid; i++) {
         R[i] = arr[mid + 1 + i]; 
     }
     
     int p1 = 0;  //S1: start~mid  S2: mid+1 ~ end 
     int p2 = 0;
     int p = start;  //temp数组的下标
     
     //比较两副牌的大小, 从每幅牌的第一张开始比较, 小的放入到新数组中
     while(p1 < mid && p2 < end) {
         if(L[p1] < R[p2]) {
             temp[p] = L[p1]; //小的元素放入到新数组中
             p1++;
            //  p++;
         } else {
             temp[p] = R[p2];
             p2++;
            //  p++;
         }
         //每判断完一次,更新新数组的下标
         p++;
     }
     
     //当左边还有牌时,就将剩余的牌添加到新数组中
     while(p1 < mid) {
         temp[p] = L[p1];
         p1++;
         p++;
     }
     //当右边还有牌时,就将剩余的牌添加到新数组中
     while(p2 < end) {
         temp[p] = R[p2];
         p2++;
         p++;
     }
}
*/ 

桶排序、基数排序

平时c语言练习

switch

#include <stdio.h>

int main() {
	int day;
	printf("输入数字:");
	scanf("%d", &day);
	
	switch(day) {
		case 1:
			printf("Monday");
			break;
		case 2:
			printf("Thusday");
			break;
		case 3:
			printf("Wednesday");
			break;
		case 4:
			printf("Thursday");
			break;
		case 5:
			printf("Friday");
			break;
		case 6:
			printf("Saturday");
			break;
		case 7:
			printf("Sunday");
			break;
		default:
			printf("滚");
			break;
	}

	
	
	
	return 0;
} 
#include <stdio.h>

int main() {
	int score;
	printf("输入成绩:");
	scanf("%d", &score);
	
	switch(score / 10) {
		case 10: 
		case 9:
				printf("A");
				break;
		case 8:
				printf("B");
				break;
		case 7:
				printf("C");
				break;
		case 6:
				printf("D");
				break;
		default: 
				printf("E");
				break;
	}
	
	/*
	if(score >= 90 && score <= 100) {
		printf("A");
	} else if(score >= 80) {
		printf("B");
	} else if(score >= 70) {
		printf("C");
	} else if(score >= 60) {
		printf("D");
	} else {
		printf("E");
	}
	*/
	
	return 0;
} 
#include <stdio.h>

int main() {
	int x;
	scanf("%d", &x);
	
	x = (x % 2 == 1) ? "Odd" : "Even";
	
	printf("%s", x);	
	
	return 0;
} 

C++ STL

STL算法基本用法

#include <iostream>
#include <algorithm>
#include <vector> //向量
#include <numeric>

using namespace std;



void PrintArray(int arr[], int size) {
  for (int i = 0; i < size; i++)
    cout << arr[i] << " ";
  cout << endl;
}
void PrintVector(vector<int> vect, int size) {
    for (int i = 0; i < size; i++)
    cout << vect[i] << " ";
  cout << endl;
}

using namespace std;

int main()
{
  int arr[] = {6, 5, 12, 10, 9, 1, 10};
  int size = sizeof(arr) / sizeof(arr[0]);
  
  //1.定义向量
  vector<int> vect(arr, arr+size);  //初始化有数组值的向量
  
  cout << "Vector is:";
  PrintVector(vect, size);
  
  //2.给向量排序——升序
  sort(vect.begin(), vect.end()); //sort函数(startAddr, endAddr)
  cout << "Vector after sorting is:";
  PrintVector(vect, size);
  
  //3.将向量中的元素反转
  reverse(vect.begin(), vect.end());
  cout << "Vector after reversing is:";
  PrintVector(vect, size);
  
  //4.向量最大值和最小值 
  cout << "向量中的最大元素为:" << *max_element(vect.begin(), vect.end()) << endl;
  cout << "向量中的最小元素为:" << *min_element(vect.begin(), vect.end()) << endl;
  
  //5.向量元素累加
  //accumulate(first_iterator, last_iterator, 初始值)
  cout << "向量中的元素总和为:" << accumulate(vect.begin(), vect.end(), 0) << endl;
  
  
  //6.计算元素个数
   cout << "元素10出现的个数为:" << count(vect.begin(), vect.end(), 10) << endl;
   
   //7.查找向量中的元素出现的个数,并指向向量的最后地址
   find(vect.begin(), vect.end(), 6) != vect.end() ? cout << "找到了" : cout << "没找到";
  /*
  sort(arr, arr + size); //startAddr endAddr
 if(binary_search(arr, arr+size, 10)) {
     cout << "找到该元素";
 } else {
    cout << "未找到该元素";
 }
 */
//   PrintArray(arr, size);
  return 0;
}

向量(vector)

向量是一个能够存放任意类型的动态数组

构造函数

  1. vector(); 创建一个空的vector
  2. vector(int n); 创建一个vector,元素个数为n
  3. vector(int n, const t& t); 创建一个vector,元素个数为n, 且值均为t
  4. vector(const vector&)复制构造函数
  5. vector(begin, end); 复制[begin,end)区间内另一个数组的元素到vector中

增加函数

  1. void push_back(const T& x) 向量尾部增加一个元素x

删除函数

  1. void pop_back()删除向量中最后一个元素
  2. void clear() 清空向量中所有元素

遍历函数

  1. iterator begin(): 返回向量头指针,指向第一个元素
  2. iterator end(): 返回向量尾指针,指向最后一个元素

判断函数

bool empty() const: 判断向量是否为空,若为空,则向量中无元素

大小函数

int size() const: 返回向量中元素的个数, 当前使用数据的大小 int capacity() const: 返回当前向量所能容纳的最大元素值,当前vector分配的大小

其他函数

  1. void swap(vector&) 交换2个同类型向量的数据
  2. void assign(int n, const T& x): 设置向量中前n个元素的值为x

基本用法

  1. Vector<类型> 标识符
  2. Vector<类型> 标识符(最大容量)
  3. Vector<类型> 标识符(最大容量,初始所有值)
  4. Vector<vector> v 二维向量
  5. 声明一个迭代器 vector::iterator it; //可以用来访问vector容器,遍历或指向容器的元素
vector<int>::iterator it;
    for(it = vec.begin(); it != vec.end(); it++)  
        cout << *it << " ";

相当于哈希表(unordered_map)

记录元素的哈希值, 根据hash值判断元素是否相同,即unodered_map内部元素是无序的。

  1. 如何使用哈希表? 添加 #include<unordered_map> 和 using namespace std;

  2. 容器模板定义

template <class Key, //键值对中的键类型
         class T,  //键值对中的值类型
         class Hash = hash<Key>, //容器内部存储值所对应的哈希函数
         class Pred = equal_to<Key>, //判断各个键值对相同的规则
         class Alloc = allocator<pair<const Key, T>> //指定分配器对象的类型
        >class unordered_map;

必须显式给前2个参数传值。

  1. 常用参数

1 . <Key,T> 存储键值对的类型(键和值)
2 . Hash = hash 指定容器在存储各个键值对时要使用的哈希函数,默认哈希函数只适用于基本数据类型。
3. Pred = equal_to 判断各个键值对是否相等的规则

创建哈希表容器

  1. 通过调用unordered_map模板类的默认构造函数,//可以创建unordered_map容器

 std::unordered_map<std::string, std::string> myMap {
     {"百度", "www.baidu.com"},
     {"谷歌", "www.google.com"}
 };

常用的成员方法

  1. begin() 返回指向容器中第一个键值对的正向迭代器
  2. end() 返回指向容器中最后一个键值对之后位置的正向迭代器
  3. empty() 若容器为空,则返回true;否则false
  4. max_size() 返回容器所能容纳键值对的最大个数
  5. size() 返回当前容器中存有键值对的个数
  6. swap() 交换 2 个 unordered_map 容器存储的键值对,前提是必须保证这 2 个容器的类型完全相等。
  7. count(key) 在容器中查找以 key 键的键值对的个数。
  8. insert()  向容器中添加新键值对。
  9. erase() 删除指定键值对。
  10. clear()清空容器,即删除容器中存储的所有键值对。
  11. at(key) 返回容器中存储的键 key 对应的值,如果 key 不存在,则会抛出 out_of_range 异常。 
  12. find(key) 查找以 key 为键的键值对,如果找到,则返回一个指向该键值对的正向迭代器;反之,则返回一个指向容器中最后一个键值对之后位置的迭代器(如果 end() 方法返回的迭代器)。
  13. hash_function() 返回当前容器使用的哈希函数对象。

auto,在编写代码时有时我们希望编译器能够帮我们自动推断变量的类型,使用关键字 auto

#include <iostream>
#include <string>
#include <unordered_map> //哈希表
using namespace std;
  
int main()
{
    //创建空的umap容器
    unordered_map<string, string> umap;
    
    //std::unordered_set<std::string> umap2;
    
    //umap2.emplace("www.4399.com");
    
    //向umap容器添加新键值对
    umap.emplace("百度", "www.baidu.com");
    umap.emplace("谷歌", "www.google.com");
    
    //输出umap存储键值对的数量
    cout << "umap size = " << umap.size() << endl;
    
    //使用迭代器输出umap容器存储的所有键值对
    for(auto iter = umap.begin(); iter != umap.end(); ++iter) {
        cout << iter->first << " " << iter->second << endl;
    }
    // cout << *
    return 0;
}