【《C Primer Plus》读书笔记】第17章:高级数据表示

news2024/11/17 5:53:33

【《C Primer Plus》读书笔记】第17章:高级数据表示

  • 17.1 研究数据表示
  • 17.2 从数组到链表
  • 17.3 抽象数据类型(ADT)
  • 17.4 队列ADT
  • 17.5 用队列进行模拟
  • 17.6 链表和数组
  • 17.7 二叉查找树
  • 17.8 其他说明

17.1 研究数据表示

在开始编写代码之前,要做很多程序设计方面的决定。

数组表示相对不灵活,在运行时确定所需内存量会更好。

假设要编写一个程序,让用户输入一年内看过的电影,存储影片的信息。可以使用结构储存电影,用结构数组存储多部电影。但给数组分配空间时,会出现分配空间过大浪费或者分配空间过小不够用的问题。使用动态内存(malloc)分配可以解决这个问题。

示例程序:

// films1.c -- 使用一个结构数组 
#include <stdio.h>
#include <string.h>

#define TSIZE 45 // 储存片名的数组大小
#define FMAX 5   // 影片的最大数量

struct film
{

    char title[TSIZE];

    int rating;
};

char *s_gets(char str[], int lim);

int main(void)

{

    struct film movies[FMAX];
    int i = 0;
    int j;

    puts("Enter first movie title:");

    while (i < FMAX && s_gets(movies[i].title, TSIZE) != NULL && movies[i].title[0] != '\0')
    {
        puts("Enter your rating <0-10>:");
        scanf("%d", &movies[i++].rating);
        while (getchar() != '\n')
            continue;
        puts("Enter next movie title (empty line to stop):");
    }

    if (i == 0)
        printf("No data entered. ");
    else
        printf("Here is the movie list:\n");

    for (j = 0; j < i; j++)
        printf("Movies: %s Rating: %d\n", movies[j].title, movies[j].rating);
    printf("Bye!\n");

    return 0;
}

char *s_gets(char *st, int n)
{
    char *ret_val;
    char *find;
    ret_val = fgets(st, n, stdin);
    if (ret_val)
    {
        find = strchr(st, '\n'); // 查找换行符
        if (find)                // 如果地址不是NULL
            *find = '\0';        // 在此处放置一个空字符 
        else
            while (getchar() != '\n')
                continue; // 处理输入行的剩余字符 
    }
    return ret_val;
}

17.2 从数组到链表

结构声明中不能有与本身类型相同的结构,但是可以有指向同类型结构的指针。

链表是由一系列结构体构成,每个结构体都有一个指针,该指针指向下一个结构。最后一个成员中此指针的值是0。

为了访问链表,需要一个单独的指针存储第一个成员的地址。

把用户接口和代码细节分开的程序更容易理解和更新。

示例程序:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define TSIZE 45 //片名大小

struct film {
  char title[TSIZE];
  int rating;
  struct film * next; //指向链表的下一个结构
};

char * s_gets(char * st, int n);


int main(void)
{
  struct film * head = NULL;
  struct film * prev = NULL, *current = NULL;
  char input[TSIZE];

  puts("输入第一部电影的名字:");
  while (s_gets(input, TSIZE) != NULL && input[0] != '\0')
  {
    current = (struct film *) malloc(sizeof(struct film));
    if (head == NULL)
      head = current;
    else
      prev->next = current;
    current->next = NULL;
    strcpy(current->title, input);
    puts("输入评分<0-10>:");
    scanf("%d", &current->rating);
    while (getchar() != '\n')
      continue;
    puts("输入下一部电影名字(直接回车可退出)");
    prev = current;

  }
  //显示电影
  if (head == NULL)
    printf("无数据.");
  else
  {
    printf("电影列表如下:\n");
    current = head;
    while (current != NULL)
    {
      printf("电影:%s 评分:%d\n", current->title, current->rating);
      current = current->next;
    }
  }

  //释放内存
  current = head;
  while (head != NULL)  //此处和书不同,书上运行出错。我认为这里应该判断head是否NULL而不是current是否为NULL
  {
    current = head;
    head =head->next;

    free(current);
  }
  printf("BYE\n");

  return 0;
}

char * s_gets(char * st, int n)
{
  char * ret_val;
  char * find;
  ret_val = fgets(st, n, stdin);
  if (ret_val)
  {
    find = strchr(st, '\n');//查找换行符
    if (find)
      *find = '\0'; //将换行符换成'\0'
    else
      while (getchar() != '\n')  //处理输入行剩余的字符
        continue;
  }
  return ret_val;
}

17.3 抽象数据类型(ADT)

类型特指两种信息:属性和操作。要定义一个新的数据类型,就必须提供存储数据的方法,还有操控数据的方法。

定义新类型的好方法是:先提供类型属性和相关操作的抽象描述。这些描述不依赖特定的实现,也不依赖特定的编程语言,称为抽象数据类型(ADT)。再开发一个实现ADT的编程接口,指明如何存储数据和执行所需操作的函数。最后编写代码实现接口。

C语言中通常的做法是,把类型定义和函数原型放在一个头文件中,该头文件提供信息。实现接口需要一个源文件,记录需要函数的细节。程序由头文件、包含处理此类型函数的源文件和主干操作的源文件组成。

对于大型项目而言,把实现和最终接口隔离的做法相当有用。

定义新类型的好方法:

  1. 提供类型属性和相关操作的抽象描述。这些描述即不能依赖特定的实现,也不能依赖特定的编程语言。这种正式的抽象描述被称为抽象数据类型(ADT)。
  2. 开发一个实现 ADT 的编程接口。即指明如何存储数据和执行所需操作的函数。
  3. 编写代码实现接口。

下面是链表的具体实现:

list.h:

//list.h
#pragma once 
#include<stdbool.h>

/*特定程序的声明*/
#define TSIZE 45 //存储电影名的数组大小
struct film
{
  char title[TSIZE];
  int rating;
};


/*一般类型定义*/

typedef struct film Item;

typedef struct node
{
  Item item;
  struct node * next;
}Node;
typedef Node * List;

/*函数原型*/

/*操作:   初始化一个链表   */
/*前提条件: plist指向一个链表 */
/*后置条件: 链表初始化为空   */
void InitializeList(List * plist);

/*操作:   确定链表是否为空定义,plist指向一个已初始化的链表 */
/*后置条件: 如果链表为空,返回ture;否则返回false       */
bool ListIsEmpty(const List * plist);

/*操作:   确定链表是否已满,plist指向一个已初始化的链表   */
/*后置条件: 如果链表已满,返回true;否则返回false       */
bool ListIsFull(const List * plist);

/*操作:   确定链表中的项数,plist指向一个已初始化的链表   */
/*后置条件: 返回链表中的项数                  */
unsigned int ListItemCount(const List *plist);

/*操作:   在链表的末尾添加项                   */
/*前提条件: item是一个待添加至链表的项,plist指向一个已初始化的链表  */
/*后置条件: 如果可以,执行添加操作,返回true;否则返回false      */
bool AddItem(Item item, List * plist);

/*操作:   把函数作用于链表的每一项                */
/*        plist指向一个已初始化的链表                */
/*        pfun指向一个函数,该函数接受一个Item类型参数,无返回值 */  
/*后置条件: pfun指向的函数作用于链表的每一项一次          */
void Traverse(const List*plist, void(*pfun)(Item item));

/*操作:   释放已分配的内存(如果有的话)             */
/*        plist指向一个已初始化的链表                */
/*后置条件: 释放为链表分配的内存,链表设置为空           */
void EmptyTheList(List * plist);

list.c:

//list.c
#include<stdio.h>
#include<stdlib.h>
#include"list.h"

static void CopyToNode(Item item, Node * pnode);

void InitializeList(List * plist)
{
  *plist = NULL;
}

bool ListIsEmpty(const List * plist)
{
  if (*plist == NULL)
    return true;
  else
    return  false;
}

bool ListIsFull(const List * plist)
{
  Node * pt;
  bool full;
  pt = (Node *)malloc(sizeof(Node));
  if (pt == NULL)
    full = true;
  else
    full = false;
  free(pt);
  return full;
}

unsigned int ListItemCount(const List * plist)
{
  unsigned int count = 0;
  Node * pnode = *plist;
  while (pnode != NULL)
  {
    ++count;
    pnode = pnode->next;
  }
  return count;
}

bool AddItem(Item item, List * plist)
{
  Node * pnew;
  Node * scan = *plist;
  pnew = (Node *)malloc(sizeof(Node));
  if (pnew == NULL)
    return false;
  CopyToNode(item, pnew);
  pnew->next = NULL;
  if (scan == NULL)
    *plist = pnew;
  else
  {
    while (scan->next != NULL)
      scan = scan->next;
    scan->next = pnew;
  }

  return true;
}

void Traverse(const List * plist, void(*pfun)(Item item)) 
{
  Node * pnode = *plist;
  while (pnode!= NULL)
  {
    (*pfun)(pnode->item);
    pnode = pnode->next;
  }
}

void EmptyTheList(List * plist)
{
  Node * psave;
  while (*plist != NULL)
  {
    psave = (*plist)->next;
    free(*plist);
    *plist = psave;
  }
}
static void CopyToNode(Item item, Node * pnode)
{
  pnode->item = item;
}

示例程序:

/*film3.c            */
/*与list.c一起编译        */
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include"list.h"
void showMovies(Item item);
char * s_gets(char * st, int n);
int main(void)
{
  List movies;
  Item temp;
  /*初始化 */
  InitializeList(&movies);
  if (ListIsFull(&movies))
  {
    fprintf(stderr, "无可用内存,告辞。\n");
    exit(1);
  }

  /*获取用户输入 并存储*/
  puts("输入第一个电影名称:");
  while (s_gets(temp.title, TSIZE) != NULL && temp.title[0] != '\0')
  {
    puts("输入你的评分<0-10>:");
    scanf("%d", &temp.rating);
    while (getchar() != '\n')
      continue;
    if (AddItem(temp, &movies) == false)
    {
      fprintf(stderr, "分配内存出错\n");
      break;
    }
    if (ListIsFull(&movies))
    {
      puts("列表满了.");
      break;
    }
    puts("输入下一步电影名称(回车结束程序)");
  }

  /*显示*/
  if (ListIsEmpty(&movies))
    printf("列表为空");
  else 
  {
    printf("Here is the movie list:\n");
    Traverse(&movies, showMovies);
  }
  printf("你输入了%d个电影\n", ListItemCount(&movies));

  /*清理*/
  EmptyTheList(&movies);
  printf("再见\n");

  return 0;
}

void showMovies(Item item)
{
  printf("Movie: %s Rating: %d\n", item.title, item.rating);
}

char * s_gets(char * st, int n)
{
  char * ret_val;
  char * find;
  ret_val = fgets(st, n, stdin);
  if (ret_val)
  {
    find = strchr(st, '\n');//查找换行符
    if (find)
      *find = '\0'; //将换行符换成'\0'
    else
      while (getchar() != '\n')  //处理输入行剩余的字符
        continue;
  }
  return ret_val;
}

17.4 队列ADT

队列是具有一些特殊属性的链表,新项只能添加到链表的末尾,只能从链表的开头移除项。队列先进先出。

17.5 用队列进行模拟

队列特性:先进先出。

示例程序:

// mall.c -- 使用Queue接口
// 和queue.c一起编译
#include <stdio.h>
#include <stdlib.h>     // 提供rand()和srand()的原型 
#include <time.h>       // 提供time()的原型 
#include "17_6_queue.h" // 更改Item的typedef 
#define MIN_PER_HR 60.0

bool newcustomer(double x);   // 是否有新顾客到来?
Item customertime(long when); // 设置顾客参数

int main(void)
{

    Queue line;             // 新的顾客数据 
    Item temp;              // 模拟的小时数 
    int hours;              // 每小时平均多少位顾客 
    int perhour;            // 每小时平均多少位顾客
    long cycle, cyclelimit; // 循环计数器、计数器的上限
    long turnaways = 0;     // 因队列已满被拒的顾客数量 
    long customers = 0;     // 加入队列的顾客数量
    long served = 0;        // 在模拟期间咨询过Sigmund的顾客数量 
    long sum_line = 0;      // 累计的队列总长 
    long wait_time = 0;     // 从当前到Sigmund空闲所需的时间 
    double min_per_cust;    // 顾客到来的平均时间 
    long line_wait = 0;     // 队列累计的等待时间 

    InitializeQueue(&line);
    srand((unsigned int)time(0)); // rand()随机初始化
    puts("Case Study: Sigmund Lander's Advice Booth");
    puts("Enter the number of simulation hours:");
    scanf("%d", &hours);
    cyclelimit = MIN_PER_HR * hours;
    puts("Enter the average number of customers per hour:");
    scanf("%d", &perhour);
    min_per_cust = MIN_PER_HR / perhour;
    for (cycle = 0; cycle < cyclelimit; cycle++)
    {
        if (newcustomer(min_per_cust))
        {
            if (QueueIsFull(&line))
                turnaways++;
            else
            {
                customers++;
                temp = customertime(cycle);
                EnQueue(temp, &line);
            }
        }
        if (wait_time <= 0 && !QueueIsEmpty(&line))
        {
            DeQueue(&temp, &line);
            wait_time = temp.processtime;
            line_wait += cycle - temp.arrive;
            served++;
        }
        if (wait_time > 0)
            wait_time--;
        sum_line += QueueItemCount(&line);
    }
    if (customers > 0)
    {
        printf("customers accepted: %ld\n", customers);
        printf("  customers served: %ld\n", served);
        printf("         turnaways: %ld\n", turnaways);
        printf("average queue size: %.2f\n", (double)sum_line / cyclelimit);
        printf(" average wait time: %.2f minutes\n", (double)line_wait / served);
    }
    else
        puts("No customers!");
    EmptyTheQueue(&line);

    return 0;
}
// x是顾客到来的平均时间(单位:分钟)
// 如果1分钟内有顾客到来,则返回true
bool newcustomer(double x)
{
    if (rand() * x / RAND_MAX < 1)
        return true;
    else
        return false;
}
// when是顾客到来的时间
// 该函数返回一个Item结构,该顾客到达的时间设置为when
// 咨询时间设置为1~3的随机值
Item customertime(long when)
{
    Item cust;
    cust.processtime = rand() % 3 + 1;
    cust.arrive = when;
    return cust;
}

17.6 链表和数组

数组是C语言直接支持的,可以随机访问,但是数组在编译时就确定大小,插入和删除元素很麻烦。链表运行时确定大小,插入删除很方便,但是不能随机访问,开发难度大。

对于一个排序的列表,二分查找的效率比顺序查找要高得多。二分查找把所有元素分为一半,比中间的小就去前半部分,比中间元素大就去后半部分,与中间的相等就算找到了,进入前半或后半部分后以此类推。

如果经常使用增删操作,使用链表更好。如果经常查找,数组更好。

数组和链表优缺点:

在这里插入图片描述

17.7 二叉查找树

二叉树的每个节点有两个指针,这两个指针指向其他节点(分别称为左节点和右节点)。

一般左节点在的项在父节点前面,右节点的项在父节点后面。如果一侧没有子节点,则指向这一侧的指针为NULL。二叉树的顶端称为根。一个节点和它的所有节点构成子树。

用二叉树每次查找就会排除一半的节点,效率高,但是更复杂。

实现:

// tree.h -- 二叉查找树

// 树种不允许有重复的项

#ifndef _TREE_H_

#define _TREE_H_

#include <stdbool.h>

// 根据具体情况重新定义Item

#define SLEN 20

typedef struct item
{

    char petname[SLEN];

    char petkind[SLEN];

} Item;

#define MAXITEMS 10

typedef struct trnode

{

    Item item;

    struct trnode *left; // 指向左分支的指针

    struct trnode *right; // 指向右分支的指针

} Trnode;

typedef struct tree

{

    Trnode *root; // 指向根节点的指针

    int size; // 树的项数

} Tree;

// 函数原型

// 操作: 把树初始化为空

// 前提条件: ptree指向一个树

// 后置条件: 树被初始化为空

void InitializeTree(Tree *ptree);

// 操作: 确定树是否为空

// 前提条件: ptree指向一个树

// 后置条件: 如果树为空,该函数返回true,否则返回false

bool TreeIsEmpty(const Tree *ptree);

// 操作: 确定树是否已满

// 前提条件: ptree指向一个树

// 后置条件: 如果树已满,该函数返回true,否则返回false

bool TreeIsFull(const Tree *ptree);

// 操作: 确定树的项数

// 前提条件: ptree指向一个树

// 后置条件: 返回树的项数

int TreeItemCount(const Tree *ptree);

// 操作: 在树中添加一个项

// 前提条件: pi是待添加项的地址,ptree指向一个一初始化的树

// 后置条件: 如果可以添加,该函数将在树中添加一个项并返回true,否则返回false

bool AddItem(const Item *pi, Tree *ptree);

// 操作: 在树中查找一个项

// 前提条件: pi指向一个项,ptree指向一个已初始化的树

// 后置条件: 如果在树中添加一个项,该函数返回true,否则返回false

bool InTree(const Item *pi, const Tree *ptree);

// 操作: 从树中删除一个项

// 前提条件: pi是删除项的地址,ptree指向一个已初始化的树

// 后置条件: 如果从树中成功删除一格项,该函数返回true,否则返回false

bool DeleteItem(const Item *pi, Tree *ptree);

// 操作: 把函数应用到树中的每一项

// 前提条件: ptree指向一个树,pfun指向一个函数,该函数接收一个Item类型的参数,并无返回值

// 后置条件: pfun咋想的这个函数为树中的每一项执行一次

void Traverse(const Tree *ptree, void (*pfun)(Item item));

// 操作: 删除树中的所有内容

// 前提条件: ptree指向一个已初始化的树

// 后置条件: 树为空

void DeleteAll(Tree *ptree);


// tree.c -- 树的支持函数

#include <string.h>

#include <stdio.h>

#include <stdlib.h>

#include "17_10_tree.h"

// 局部数据类型

typedef struct pair
{

    Trnode *parent;

    Trnode *child;

} Pair;

// 局部函数的原型

static Trnode *MakeNode(const Item *pi);

static bool ToLeft(const Item *i1, const Item *i2);

static bool ToRight(const Item *i1, const Item *i2);

static void AddNode(Trnode *new_node, Trnode *root);

static void InOrder(const Trnode *root, void (*pfun)(Item item));

static Pair SeekItem(const Item *pi, const Tree *ptree);

static void DeleteNode(Trnode **ptr);

static void DeleteAllNodes(Trnode *ptr);

// 函数定义

void InitializeTree(Tree *ptree)

{

    ptree->root = NULL;

    ptree->size = 0;
}

bool TreeIsEmpty(const Tree *ptree)

{

    if (ptree->root == NULL)

        return true;

    else

        return false;
}

bool TreeIsFull(const Tree *ptree)

{

    if (ptree->root == NULL)

        return true;

    else

        return false;
}

int TreeItemCount(const Tree *ptree)

{

    if (ptree->size == MAXITEMS)

        return true;

    else

        return false;
}

bool AddItem(const Item *pi, Tree *ptree)

{

    Trnode *new_node;

    if (TreeIsFull(ptree))

    {

        fprintf(stderr, "Tree is full\n");

        return false; // 提前返回
    }

    if (SeekItem(pi, ptree).child != NULL)

    {

        fprintf(stderr, "Attempted to add duplicate item\n");

        return false; // 提前返回
    }

    new_node = MakeNode(pi); // 指向新节点

    if (new_node == NULL)

    {

        fprintf(stderr, "Couldn't create node\n");

        return false; // 提前返回
    }

    // 成功创建了一个新节点

    ptree->size++;

    if (ptree->root == NULL) // 情况1:树为空

        ptree->root = new_node; // 新节点为树的根节点

    else // 情况2:树不为空

        AddNode(new_node, ptree->root); // 在树中添加新节点

    return true; // 成功返回
}

bool InTree(const Item *pi, const Tree *ptree)

{

    return (SeekItem(pi, ptree).child == NULL) ? false : true;
}

bool DeleteItem(const Item *pi, Tree *ptree)

{

    Pair look;

    look = SeekItem(pi, ptree);

    if (look.child == NULL)

        return false;

    if (look.parent == NULL) // 删除根节点项

        DeleteNode(&ptree->root);

    else if (look.parent->left == look.child)

        DeleteNode(&look.parent->left);

    else

        DeleteNode(&look.parent->right);

    ptree->size--;

    return true;
}

void Traverse(const Tree *ptree, void (*pfun)(Item item))

{

    if (ptree != NULL)

        InOrder(ptree->root, pfun);
}

void DeleteAll(Tree *ptree)

{

    if (ptree != NULL)

        DeleteAllNodes(ptree->root);

    ptree->root = NULL;

    ptree->size = 0;
}

// 局部函数

static void InOrder(const Trnode *root, void (*pfun)(Item item))

{

    if (root != NULL)

    {

        InOrder(root->left, pfun);

        (*pfun)(root->item);

        InOrder(root->right, pfun);
    }
}

static void DeleteAllNodes(Trnode *root)

{

    Trnode *pright;

    if (root != NULL)

    {

        pright = root->right;

        DeleteAllNodes(root->left);

        free(root);

        DeleteAllNodes(pright);
    }
}

static void AddNode(Trnode *new_node, Trnode *root)

{

    if (ToLeft(&new_node->item, &root->item))

    {

        if (root->left == NULL) // 空子树

            root->left = new_node; // 把结点添加到此处

        else

            AddNode(new_node, root->left); // 否则处理该子树
    }

    else if (ToRight(&new_node->item, &root->item))

    {

        if (root->right == NULL) // 空子树

            root->right = new_node; // 把结点添加到此处

        else

            AddNode(new_node, root->right); // 否则处理该子树
    }

    else // 不允许有重复项

    {

        fprintf(stderr, "location error in AddNode()\n");

        exit(1);
    }
}

static bool ToLeft(const Item *i1, const Item *i2)

{

    int comp1;

    if ((comp1 = strcmp(i1->petname, i2->petname)) < 0)

        return true;

    else if (comp1 == 0 && strcmp(i1->petkind, i2->petkind) < 0)

        return true;

    else

        return false;
}

static bool ToRight(const Item *i1, const Item *i2)

{

    int comp1;

    if ((comp1 = strcmp(i1->petname, i2->petname)) > 0)

        return true;

    else if (comp1 == 0 && strcmp(i1->petkind, i2->petkind) > 0)

        return true;

    else

        return false;
}

static Trnode *MakeNode(const Item *pi)

{

    Trnode *new_node;

    new_node = (Trnode *)malloc(sizeof(Trnode));

    if (new_node != NULL)

    {

        new_node->item = *pi;

        new_node->left = NULL;

        new_node->right = NULL;
    }

    return new_node;
}

static Pair SeekItem(const Item *pi, const Tree *ptree)

{

    Pair look;

    look.parent = NULL;

    look.child = ptree->root;

    if (look.child == NULL)

        return look; // 提前返回

    while (look.child == NULL)

    {

        if (ToLeft(pi, &(look.child->item)))

        {

            look.parent = look.child;

            look.child = look.child->left;
        }

        else if (ToRight(pi, &(look.child->item)))

        {

            look.parent = look.child;

            look.child = look.child->right;
        }

        else // 如果前两种情况都不满足,则必定是相等的情况

            break; // look.child目标项的结点
    }

    return look; // 成功返回
}

static void DeleteNode(Trnode **ptr) // ptr是指向目标节点的父节点指针成员的地址

{

    Trnode *temp;

    if ((*ptr)->left == NULL)

    {

        temp = *ptr;

        *ptr = (*ptr)->right;

        free(temp);
    }

    else if ((*ptr)->right == NULL)

    {

        temp = *ptr;

        *ptr = (*ptr)->left;

        free(temp);
    }

    else // 被删除的结点有两个子节点

    {

        // 找到重新连接右子树的位置

        for (temp = (*ptr)->left; temp->right != NULL; temp = temp->right)

            continue;

        temp->right = (*ptr)->right;

        temp = *ptr;

        *ptr = (*ptr)->left;

        free(temp);
    }
}

17.8 其他说明

花时间查看你的系统提供什么。如果没有你想要的工具,就自己编写函数,这是 C 的一部分。如果认为自己能编写一个更好的,那就去做!随着你不断练习并提高自己的编程技术,会从一名新手称为经验丰富的资深程序员。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/419806.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【2023】前端JWT详解

概述 回顾登录的流程&#xff1a; 接下来的问题是&#xff1a;这个出入证&#xff08;令牌&#xff09;里面到底存啥&#xff1f; 一种比较简单的办法就是直接存储用户信息的JSON串&#xff0c;这会造成下面的几个问题&#xff1a; 非浏览器环境&#xff0c;如何在令牌中记录…

C/C++每日一练(20230416)

目录 1. 求数列第n项值 ※ 2. 整数转换英文表示 &#x1f31f;&#x1f31f;&#x1f31f; 3. 数组中找出最大值及索引位置 ※ &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1. 求数…

微信小程序引入骨架屏

骨架屏的使用越来越广泛。在微信小程序中使用骨架屏如果自己实现&#xff0c;不同的页面对应不同的骨架屏&#xff0c;会有一定难度&#xff1b;不过&#xff0c;微信小程序已经提供生成骨架屏功能&#xff0c;使得我们在开发中非常方便&#xff0c;下面介绍如何生成 在生成骨架…

[Linux]管理用户和组

​⭐作者介绍&#xff1a;大二本科网络工程专业在读&#xff0c;持续学习Java&#xff0c;输出优质文章 ⭐作者主页&#xff1a;逐梦苍穹 ⭐所属专栏&#xff1a;Linux基础操作。本文主要是分享一些Linux系统常用操作&#xff0c;内容主要来源是学校作业&#xff0c;分享出来的…

JavaEE 协议 信息是如何在一个机器传到另一个机器的(理论)

抓住你了&#xff01; 文章目录JavaEE & 协议 & 信息是如何在一个机器传到另一个机器的1. 局域网2. 广域网3. IP与端口号&#xff08;初识&#xff09;4. 协议4.1 协议分类分层4.2 协议分层的好处4.3 真实的网络协议分层&#xff0c;TCP/IP五层网络模型4.3.1 应用层4.3…

Spark 3.0中 Spark SQL优化

在Spark3.x版本提供Adaptive Query Execution自适应查询技术&#xff0c;通过在”运行时”对查询执行计划进行优化&#xff0c;允许Planner在运行时执行可选计划&#xff0c;这些可选计划将会基于运行时数据统计进行动态优化&#xff0c;从而提高性能。 Adaptive Query Execut…

在DongshanPI-D1开箱使用分享与折腾记录实现MPU6050数据读取

前言 上一篇文章使用RT-Smart的IIC驱动OLED屏幕&#xff0c;进行基本的字符串显示,在使用过程中对RT-Smart有了一定熟悉&#xff0c;准备使用SPI驱动ST7789&#xff0c;但SPI接口没有引出&#xff0c;本次使用手上已有的传感器MPU6050进行使用。 过程 本次直接开始添加离线包…

NDK RTMP直播客户端二

在之前完成的实战项目【FFmpeg音视频播放器】属于拉流范畴&#xff0c;接下来将完成推流工作&#xff0c;通过RTMP实现推流&#xff0c;即直播客户端。简单的说&#xff0c;就是将手机采集的音频数据和视频数据&#xff0c;推到服务器端。 接下来的RTMP直播客户端系列&#xff…

在国内pmp证书有什么含金量?

关于PMP的含金量&#xff0c;很多回答的说法都差不多&#xff0c;但那也只是字面上的含金量&#xff0c;真正的含金量还是得自己考过了之后能够给自己带来的帮助才方可对PMP含金量下定义&#xff0c;但能一眼就能看到的含金量是在一些招聘信息上关于PMP证书的要求&#xff0c;下…

【Axure教程】日期时间下拉列表

在系统中&#xff0c;我们经常会用到日期时间选择器&#xff0c;它同时包含了日历日期的选择和时间的选择&#xff0c;一般是下拉列表的形式进行选择。 今天作者就教大家如何在Axure中用中继器制作真实日期时间效果的下拉列表选。 一、效果展示 1、点击控件&#xff0c;可以…

游戏开发学习路线图(2023最新版)建议收藏

游戏开发是一个高度技术化的领域&#xff0c;需要掌握编程语言和编程技能。你可以从学习基本的编程概念和语法开始&#xff0c;如C、C#、Python等常用的游戏编程语言。掌握编程的基础知识是游戏开发的基石。很多小伙伴不知道怎么学习游戏开发&#xff0c;那么今天&#xff0c;就…

c/c++:windows平台下依赖的动态库,c底层是汇编语言,程序断点调试,反汇编,vs快捷键

c/c&#xff1a;windows平台下依赖的动态库&#xff0c;c底层是汇编语言&#xff0c;程序断点调试&#xff0c;反汇编&#xff0c;vs快捷键 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;此时学会c的话&#xff0c; 我所知…

【多媒体】多媒体架构

多媒体架构 首先,多媒体架构层通常由三个主要层次组成: 应用层:负责媒体的展示、控制和交互等功能,如播放器、视频编辑器等。应用层一般是用户最直接接触到的界面。 中间件层:提供了各种媒体操作的基本服务,如编解码、音频合成、图像处理、网络传输、数据存储等。中间件…

【图像分割】Meta分割一切(SAM)模型环境配置和使用教程

注意&#xff1a;python>3.8, pytorch>1.7,torchvision>0.8 Feel free to ask any question. 遇到问题欢迎评论区讨论. 官方教程&#xff1a; https://github.com/facebookresearch/segment-anything 1 环境配置 1.1 安装主要库&#xff1a; &#xff08;1&…

day08_数组

今日内容 零、 复习昨日 一、作业 二、数组 零、 复习昨日 方法/函数是什么? 方法就是完成某个功能的一段代码的集合例如: 方法是 做饭 方法的作用是什么? 封装的思想方便维护方便复用 方法的参数列表是什么意思? 参数:方法执行所需的数据参数列表: 方法执行可以需要多个数据…

Linux下C/C++ SNTP网络时间协议实现

对于许多应用程序&#xff0c;特别是在小型计算机和微控制器上&#xff0c;不需要NTP的最终性能。便开发了简单网络时间协议&#xff08;SNTP&#xff09;&#xff0c;为功能较弱的计算机提供时钟同步&#xff0c;而这些计算机不需要NTP的复杂性。 而简单网络时间协议&#xf…

简易糖尿病胰岛素注射量推荐系统运行记录(github项目)

前言 在github上找案例推理相关实现代码&#xff0c;找到这个项目&#xff0c;记录一下运行过程。项目地址&#xff1a;https://github.com/jcf-junior/Diabetes-CBR 运行记录 运行项目的前提是已经装好的所有request的包&#xff0c;电脑里已经安装过mongodb数据库。 原项目…

Linux XFS文件系统的备份与还原

文章目录Linux XFS文件系统的备份与还原XFS文件系统备份xfsdump语法xfsdump备份完整的文件系统用xfsdump进行增量备份XFS文件系统还原xfsrestore语法用xfsrestore观察和xfsdump后的备份数据内容简单恢复level 0 的文件系统恢复增量备份数据仅还原部分文件到xfsrestore交互模式L…

深入剖析Android视图层次结构,为什么UI界面如此多样化?

简述 在Android Framework中&#xff0c;渲染机制是指如何为应用程序的用户界面绘制和布局视图&#xff08;View&#xff09;。Android的视图层次结构&#xff08;View Hierarchy&#xff09;是由视图树中的每个节点表示的 。当更新视图树时&#xff0c;Android会执行以下流程…

记一次内存泄漏问题的排查

阶段一&#xff1a; 前段时间&#xff0c;突然发现服务在毫无征兆的情况下发生了重启。去看了一下容器退出的日志&#xff0c;发现内存利用率超过了100%&#xff0c;导致容器重启&#xff0c;进一步看了skyWalking&#xff0c;发现heap内存超了&#xff0c;当时只是简单的以为是…