<![CDATA[算法题整理]]>https://ariser.cn/index.php/archives/354/

剑指Offer

【剑指Offer】T7 重建二叉树
【剑指Offer】T9 两个栈实现队列
【剑指Offer】T11 旋转数组最小的数字
【剑指Offer】T14 剪绳子(动态规划、贪婪算法)
【剑指Offer】T15 二进制中1的个数(位运算)
【剑指Offer】T16 数值的整数次方
【剑指Offer】T18 删除链表节点
【剑指Offer】T21 调整数组顺序使奇数位于偶数前面
【剑指Offer】T30 包含min函数的栈
【剑指Offer】T31 栈的压入、弹出序列
【剑指Offer】T32 从上往下打印二叉树(层次遍历)
【剑指Offer】把二叉树打印成多行
【剑指Offer】T33 判断是否是二叉搜索树的后序序列
【剑指Offer】T34 二叉树中和为某一值的路径
【剑指Offer】T35 复杂链表的复制
【剑指Offer】T36 二叉搜索树和双向链表
【剑指Offer】T39数组中出现次数超过一半的数字
【剑指Offer】T40 最小的K个数
【剑指Offer】T41 数据流中的中位数
【剑指Offer】T42 连续子数组最大和
【剑指Offer】T43 1~n整数中1出现的次数
【剑指Offer】数组中只出现一次的数字
【剑指Offer】T65 不用加减乘除实现加法
【剑指Offer】数组中重复的数字(哈希)
【剑指Offer】T50 第一个只出现一次的字符(哈希)
【剑指Offer】T51 数组中的逆序对

LeetCode

【LeetCode】T43 1~n整数中1出现的次数
【LeetCode】T56 合并区间
【LeetCode】T98. 验证二叉搜索树
【LeetCode】T104.二叉树最大深度
【LeetCode】105、106.构建二叉树
【LeetCode】T108有序数组转为二叉搜索树
【LeetCode】109. 有序链表转换二叉搜索树 - 快慢指针对链表求中间点
【LeetCode】T111.二叉树最小深度
【LeetCode】287. 寻找重复数 --- 数组求重复
【LeetCode】946. 验证栈序列

]]>
算法题
<![CDATA[数据结构-查找排序整理]]>https://ariser.cn/index.php/archives/348/

查找

  • 线性结构

    • 顺序查找:从头到尾线性搜索
    • 折半查找:高低mid指针,向中间逼近
    • 分块查找:分成若干子块,拿出最大/小值作为索引。先搜索引找块,再块内搜索
  • 树形结构

    • 二叉排序树:左子树小,右子树大
    • 二叉平衡树:左右深度不超过1
    • 红黑树:平衡树的优化,三次旋转。
  • 散列结构 - 散列表

    • 性能分析
    • 冲突分析
  • 效率指标 - 平均查找长度

    • 查找成功
    • 查找失败

顺序查找

内容:从头到尾线性遍历

时间复杂度(平均):O(n)

折半查找

复杂度:O(logn)

条件:有序数组

内容:

  • 设置高低指针
  • 中间开始,如果值相等则结束
  • 值不相等,高低指针向中间迫近
  • 指针相遇,结束

代码:

int Binary_Search(int A[], int key){
    int low = 0, high = A.size() - 1, mix = 0; // 高低指针
    // 指针未相遇时循环
    while(low <= high){
        mid = low + (high - low)/2;
        if(A[mid] == key)
            return mid;
        else if(A[mid] < key)
            low = mid + 1;
        else
            high = mid - 1;
    }//while
    return -1;
}

二叉排序树查找 ==插入和删除==

复杂度:O(logn)

条件:二叉排序树

内容:

  • 树空,查找失败
  • key为根节点值,查找成功
  • key小于根节点,查找左子树
  • 查找右子树

代码:

// 非递归
BSTNode* BST_Search(BSTNode* root, int key){
    BSTNode* p = root;
    while(p){
        if(p->data == key)
            return p;
        if(p->data < key)
            p = p->rchild;
        else
            p = p->lchild;
    }
    return NULL;
}

// 递归
BSTNode* BST_Search(BSTNode* root, int key){
    if(root == NULL)
        return NULL;
    
    if(root->data == key)
        return root; // 出口
    else if(root->data > key)
        BST_Search(root->lchild, key);
    else
        BST_Search(root->rchild, key);
}

删除:

插入:

分块查找

内容:

  • 元素划分为m块,块内不必有序,块与块之间有序(块1max<块2所有
  • 查找时,先二分法查索引(块),再线性搜索块内。

平均查找长度:[logs(b_1)] + (s+1)/2上取整。

哈希(散列表)==代码?==

内容:

  • Hash(key) = Addr,通常用除留余
  • 用散列函数数据映射到数组下标、地址、索引

    • 函数会起冲突。==冲突越少,占用空间越多==
    • 使用开地址法和拉链法。

      • 位置有元素,往后再用函数运算后的结果+diHash()找位置
      • H = (H(key) + di)%m di可以是0、1、2、3也可以是 0方、1方、-1方、2方、-2方
  • 查找的时候直接用相同的函数求索引。
  • 链地址犹豫开地址,除留余优于其他散列函数

时间复杂度:理想情况O(1)(无冲突)

==KMP 算法==

排序

  • 插入排序

    • 直接插入:往后遍历,判断A[i]“已排序序列”中的位置,再插入进去。|有序序列|A[i]|无序序列| n^2
    • 折半插入:减小了比较次数。插入位置使用折半查找的方法。nlog2n
    • 希尔排序:每隔五个一分(16、27、38、49),组内排序。再三个一分,再到一个一分就排序成功
  • 交换排序

    • 冒泡排序:两个指针从后往前,小的交换在前面。注意设置哨兵判断结束情况。
    • 快速排序:选择枢纽,小的放在左边,大的放在右边。递归操作。(高低指针向中间迫近,低指针碰到大于的停下,等高指针碰到小于枢纽的,最后交换位置。最最后放入枢纽值)
  • 选择排序

    • 简单排序:选出最小的丢到头部。
    • 堆排序:需要维护一个堆,只能取堆顶元素。
  • 归并排序:全部分成两个一组,组内排序。再四个一组,组内排序(组内前后两个指针,类似快排向中间逼近)。最后到整个数组。
  • 基数排序

849589-20180402133438219-1946132192.png

直接插入排序

直接插入排序
  • 内容:往后遍历,判断A[i]“已排序序列”中的位置,再插入进去。|有序序列|A[i]|无序序列| n^2
  • 复杂度:O(n^2)前到后遍历+找插入位置。n(n-1)/2
  • 稳定,适用大多数线性表
  • 代码:

    void InsertSort(int A[], int n){
        for(int i = 1; i < n; i++){
            int temp = A[i]; // 待排关键字
            
            int j = i - 1; // 后往前找位置
            while(j >= 0 && temp < A[j]){ // 大于待排关键字,后移
                A[j+1] = A[j];
                j--;
            }
            R[j+1] = temp; // 插入位置
        }
    }
折半插入排序
  • 内容:相当于简单插入排序,折半是对找插入位置进行了优化,使用折半查找(因为已排序序列是有序的)。
  • 复杂度:最好O(nlog2n),最坏O(n^2),综合O(n^2),因为移动次数还是那么多,移动次数最少的时候为O(nlog2n)
  • 稳定,适用大多数线性表
  • 代码:

    void InsertSort(int A[], int n){
        for(int i = 1; i < n; i++){
            int temp = A[i]; // 待排关键字
            int low = 0, high = i - 1, mid = 0;
            while(low <= high){
                int mid = low + (high - low)/2;
                if(A[low] < temp)
                    low = mid + 1;
                else
                    high = mid - 1;
            }
            // 后移 mid到i-1元素后移
            for(int j = i-1; j > mid; j--)
                A[j+1] = A[j];
            A[mid] = temp; // 插入位置
        }
    }

交换排序

冒泡排序
  • 内容:从后往前,两个指针比较,把小的送到最前面。设置哨兵记录排序完成
  • 复杂度:最坏情况逆序:O(n^2)。比较次数:n(n-1)/2,移动次数:3n(n-1)/2(每次移动三次元素)
  • 稳定性:稳定
  • 其他:每趟排序,都有元素在最终位置
  • 代码:

    void BubSort(int A[], int n){
        for(int i = 0; i < n-1; i++){ // i指的是每轮排好的个数|i个已排好|n-i个未排好|,要遍历n-1次
            int flag = 0;
            for(int j = n-1; j > i; j++){
                if(A[j] < A[j-1]){// 逆序,交换次序
                    int temp = A[j];
                    A[j] = A[j-1];
                    A[j-1] = temp;
                    flag = 1;
                }
                // 从尾到头都没发生交换,证明已经有序
                if(flag == 0)
                    return;
            }
        }
    }
快速排序
  • 内容:找枢纽值,小的丢到左边,大的丢到右边。再递归对左右进行操作
  • 复杂度:递归操作O(logn),分类操作 O(n),故 O(nlogn)

    • 基本有序的序列,退化为冒泡排序
    • 空间复杂度为:借助栈,最好的情况[log2(n-1)]↑(栈深度),最坏O(n-1)(基本有序,n-1次递归),所以为O(log2n)
  • 稳定性:不稳定
  • 算法描述:高低指针向中间迫近(符合条件时,低小于枢纽,高大于枢纽)。一方先走,不符合就停下来,等另一方走到不符合。然后交换位置。
  • 代码:

    void QuickSort(int A[], int low, int high){
        if(low < high){
            int privot = Partition(A, low, high); // 划分
            QuickSort(low, pivot-1); // 左右子表递归,记得是low~pivot-1和pivot+1~high,中间是枢纽值。
            QuickSort(pivot+1, high);
        }
    }
void Partition(int A[], low, high){
    int pivot = A[low]; // 放入临时区
    while(low < high){
        // 注意是高指针先移动,才能保证正确交换。因为A[low]放入缓冲区了,所以这个位置可以先放数据
        while(low < high && A[high] >= pivot)
            high--;
        A[low] = A[high];
        while(low < high && A[low] <= pivot)
            low++;
        A[high] = A[low];
    }
    A[low] = pivot; // low后移动的,决定最终位置
    return low;
}

选择排序

简单选择排序

内容:选最小的丢到前面

复杂度:比较次数始终是n(n-1)/2,移动操作不超过3(n-1),最好情况不移动。复杂度:O(n^2)

稳定性:不稳定

代码:

void SelectSort(int A[], int n){
    for(int i = 0; i < n; i++){
        int min = 0; // 最小值的位置
        for(int j = ){
            min =i;
            for(int j = i+1; j < n; j++){ // 更新最小值
                if(A[j]< A[min])
                    min = j;
            }
            // 关键字交换
            int temp = A[min];
            A[min] = A[i];
            A[i] = temp;
        }
    }
}
堆排序
  • 内容:

    • 堆:父节点小于子节点(最小堆)或父节点大于子节点(最大堆)
    • 每次只能取堆顶元素
    • 需要维护堆。添加的时候,新节点放在最尾部(类似完全二叉树),然后交换位置直到满足条件。
  • 复杂度:建堆O(n),总的为 O(nlog2n)
  • 稳定性:不稳定
  • 代码:

    // A[low] 到 A[high] 的范围对low上的节点进行调整
    void Sift(int A[], int low, int high){
        int i = low, j = 2*i; // A[j] 是 A[i] 的左孩子
        int temp = A[i];
        
        while(j <= high){
            // 有孩子较大,j指向右孩子
            if(j < high && A[j] < A[j+1])
                j++;
            if(temp < A[j]){
                A[i] = A[j]; // A[j]调整到双亲位置
                i = j; // 修改i和j的值,以便继续向下
                j = 2*i;
            }else
                break; // 调整结束
        }
        A[i] = temp; // 被调整节点值放入最终位置
    }
    
    void heapSort(int R[], int n){
        int i;
        for(i = n/2; i >= 1; i--)
            Sift(A, i, n);//调整堆
        for(i = n; i >=2; i--){
            // 换出了根节点的关键字,将其放入最终位置
            int temp = A[1];
            A[1] = A[i];
            A[i] = temp;
            Sift(A, 1, i-1); // 在减少了一个关键字的无序序列中进行调整
        }
    }

归并排序

  • 内容:全部分成两个一组,组内排序。再四个一组,组内排序(组内前后两个指针,类似快排向中间逼近)。最后到整个数组。
  • 复杂度:O(nlog2n)log2n趟排序,每趟n次操作。
  • 空间复杂度:O(n),因为需要转存整个待排序列
  • 代码

    void merge(int *a, int low, int mid, int high, int *temp)
    {
        int i = low, j = mid+1;
        int k = 0;
        while(i <= mid && j <= high){
            if(a[i] <= a[j])
                temp[k++] = a[i++];
            else
                temp[k++] = a[j++];        
        }
        // 剩下的没归并进去的,直接复制
        while(i <= mid)
            temp[k++] = a[i++];
        while(j <= high)
            temp[k++] = a[j++];
        
        for(i = 0; i < k; i++)
            a[low+i] = temp[i]; // 复制到A
        
    }
     
    void mergeSort(int *A, int low, int high, int *temp)
    {    
        // 当left==right的时,已经不需要再划分了,出口
        if(low < last)
        {
            int mid = (low + high)>>1;
            mergeSort(A, low, mid, temp); // 左侧子序列递归
            mergeSort(A, mid+1, high, temp); // 右侧子序列递归
            merge(A, low, mid, high, temp); // 归并,A的low~mid, mid+1~high两段有序的归并成一段
        }
    }

树算法

遍历

// 递归先序遍历,visit()位置代表遍历类型
void Order(BTNode *p){
    if(p!=NULL){
        visit(p);
        Order(p->lchild);
        Order(p->rchild);
    }
}

层次遍历

思路:

  • 根节点入队
  • 队不空

    • 队头元素出队
    • 访问当前元素
    • 左右子树只要存在,就入队。先左后右
void LeveOrder(BTNode *root){
    if(!root)
        return;
    
    queue<BTNode*> Q;
    
    Q.push(root); // 根节点入队
    while(!Q.empty()){
        BTNode* q = Q.front();
        // cout<<q->data;
        Q.pop();
        // 左右子树根入队
        if(q->left)
           Q.push(q->left);
        if(q->right)
           Q.push(q->right);
    }
}

求二叉树深度

int getDepth(BTNode *p){
    if(p == NULL)
        return 0;
    else {
        int LD = getDepth(p->lchild);
        int RD = getDepth(p->rchild);
        return (LD>RD ? LD : RD) + 1; // 左右子树深度最大值加1(根节点)
    }
}

判断二叉树是否满

int IsFull(BTNode *bt){
    if(bt){
        // 只有根节点
        if(bt->lchild == NULL && bt->rchild == NULL)
            return 1;
        else if(bt->lchild == NULL || bt->rchild == NULL) // 左为空或右为空,单支
            return 0;
        else
            return (IsFull(bt->lchild) && IsFull(bt->rchild)) // 递归遍历左右子树
    }
}

判断二叉树是否平衡

int getDepth(BTNode *bt){
    if(bt == NULL)
        return 0;
    else {
        int LD = getDepth(bt->lchild);
        int RD = getDepth(bt->rchild);
        return (LD>RD ? LD:RD) + 1;
    }
}

int AVL(BTNode *bt){
    if(bt == NULL)
        return 1;
    int ld = getDepth(bt->lchild);
    int rd = getDepth(bt->rchild);
    
    int gap = ld - rd;
    if(gap<-1 || gap>1)
        return 0
    else
        return AVL(p->lchild) && AVL(p->rchild); 左右子树均返回1才能return,先左子树后右子树。
}

判断二叉树是不是二叉排序树

// 二叉排序树中序遍历有序
long last = LONG_MIN; // 父节值
bool flag = true; // 父结点是否大于子节点
bool IsBSTree(TreeNode* root){
    if(!root)
        return true;
    
    // 遍历左子树
    if(flag && root->left)
        IsBSTree(root->left);
    
    // 做判断
    // 当前节点小于等于上一个节点,不是二叉排序树
    if(root->val <= last)
        flag = false;
    last = root->data; // 记录父节点值
    
    //遍历右子树
    if(root->rchild && flag != 0)
        IsBSTree(root->right);
    
    // 树都遍历完 或 不是二叉排序树,就退出
    return flag;
}

判断两棵树是否相等

bool isSameTree(TreeNode* p, TreeNode* q){
    if(!p && q)
        return false;
    if(!q && p)
        return false;
    if(!p && !q)
        return true;
    
    if(p->val == q->val)
        return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
    return false;
}

图算法

深度优先遍历

  • 代码:

    int visited[maxSize];
    // v是起始节点
    // 
    void DFSTraverse(Graph* G, int v){
        Graph* p;
        visit[v] = 1; // 标记已访问
        Visit(v); // 访问顶点
        p = g->adjlist[v].firstarc; // p指向顶点v的第一条边
        while(p != NULL){
            if(visit[p->adjvex] == 0) // 若访问的顶点未访问,递归访问它
                DFS(G, p->adjvex);
            p = p->nextarc; // p指向下一条边的终点
        }
    }
]]>
数据结构
<![CDATA[数据库-事务及其隔离问题]]>https://ariser.cn/index.php/archives/420/
多条语句同时成功或同时不成功,有一条失败会回滚,所有事务操作取消

  • 用 BEGIN, ROLLBACK, COMMIT 来实现

    • BEGIN 或 START TRANSACTION; 开始一个事务
    • ROLLBACK 事务回滚
    • COMMIT 事务确认
  • 直接用 SET 来改变 MySQL 的自动提交模式:

    • SET AUTOCOMMIT=0 禁止自动提交
    • SET AUTOCOMMIT=1 开启自动提交
START TRANSACTION; // 开启事务
INSERT INTO stu (class_id,sname,sex)VALUES(2,'xx','x');
COMMIT;

四大特性

  • 原子性:不能部分执行,事务不能完成也要回滚消除影响

    • 侧重事务本身的职责,不管具体内容,只管能不能完成事务。
  • 一致性:事务操作就是把数据库从一个状态到另一个状态。

    • 侧重事务完成的结果
    • 比如A和B都有1000,总共2000。A向B转账500,那么必须保持事务发生后A B都为1000

      • A 减少500 和 B增加500
      • 这两个操作必须在事务内全部得到实现
  • 隔离性:多事务并发运行,各自执行各自的,互不影响
  • 持久性:数据库出错也能恢复,或者事务提交后不会再发生意外改变

隔离问题

  • 脏读:A读取了B事务过程中(未提交)的数据,但B后面进行了回滚。A读取到的即为脏数据
  • 不可重复读:相对于update

    • A多次读取同一数据
    • B的事务在A事务<u>多次读取的过程中</u>进行了更新<u>并提交</u>
    • A多次读取的结果不一致
  • 幻读:相对于 insertdelete操作

    • A将数据库中所有成绩从具体分数改为ABCD等级
    • B这个时候插入了具体分数记录
    • A改结束,发现有一条记录没改过来,像出现幻觉一样

不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增删除

解决<u>不可重复读</u>的问题只需<u>锁住满足条件的</u>行,解决<u>幻读</u>需要锁表<u>四种隔离级别</u>

隔离级别

事务隔离级别脏读不可重复读幻读说明
读未提交(read-uncommitted)最低的事务隔离级别,一个事务还没提交时,它做的变更就能被别的事务看到
不可重复读(read-committed)保证一个事物提交后才能被另外一个事务读取。另外一个事务不能读取该事物未提交的数据。
可重复读(repeatable-read)多次读取同一范围的数据会返回第一次查询的快照,即使其他事务对该数据做了更新修改。事务在执行期间看到的数据前后必须是一致的。
串行化(serializable)事务 100% 隔离,可避免脏读、不可重复读、幻读的发生。花费最高代价但最可靠的事务隔离级别。

控制和查询

select @@tx_isolation; // 查询隔离级别
set session transaction isolation level read uncommitted; // 设置隔离级别

PHP实现

// mysql
mysql_query("COMMIT");//提交事务
mysql_query("ROLLBACK");//至少有一条sql语句执行错误,事务回滚
mysql_query("END");//事务结束

// mysqli
$conn = mysqli_connect('127.0.0.1', 'root', 'root') or die(mysqli_error());  //连接数据库 
mysqli_query($conn, 'BEGIN');    //开启事务
mysqli_query($conn, 'COMMIT');    //提交事务
mysqli_query($conn, 'rollBack');    //回滚事务

//PDO
$pdo->beginTransaction(); //开启事务 
$pdo->commit(); //提交事务  
$pdo->rollBack(); //回滚事务  
$pdo->end(); //结束事务  


// Laravel
// 方法一,transaction方法
DB::transaction(function () {
    DB::table('users')->update(['votes' => 1]);
    DB::table('posts')->delete();
});

// 方法二、手动事务
DB::beginTransaction();  //开启事务
DB::commit();  //事务提交
DB::rollBack();  // 事务回滚 
]]>
数据库
<![CDATA[计算机图形图像技术-复习参考题代码部分答案]]>https://ariser.cn/index.php/archives/132/

前言

环境:Clion + OpenGL + OpenCV

按照书中代码进行复现或改造,如果有错误,为保证即时性,请在博客文章下方留言,会及时更正。

Edited by Aris. Last edited on July 6, 2019

OpenGL

T3

题目:请使用 OpenGL 和 GLUT 编写一个简单的图形程序,用于显示一个填充的白色矩形。其中矩形规定为(-0.8, -0.8)~(0.8, 0.8),程序窗口的大小为(200, 200),标题为“白色矩形”。

#include <GL/glu.h>
#include <GL/glut.h>

void Paint(){
    glClear(GL_COLOR_BUFFER_BIT);// 初始化颜色缓冲区
    // glColor3f(1, 0, 0); // 设置颜色为红色
    glRectf(-0.8, -0.8, 0.8, 0.8);
    glFlush();
}

int main(int argc, char **argv){
    glutInit(&argc, argv); // 做题不写
    glutInitWindowSize(200, 200); // 程序窗口大小
    glutCreateWindow("白色矩形"); // 窗口标题
    glutDisplayFunc(Paint); // 执行场景绘制函数
    glutMainLoop(); // 开始循环执行 OpenGL 命令
}
T4

题目:请使用 OpenGL 和 GLUT 编写一个简单的图形程序,用于显示一个填充的红色三角形。其中三角形的顶点分别是(-0.8, -0.8)、(0.8, -0.8)和(0, 0.8),程序窗口大小为(200, 200),标题为“红色三角形”

#include <GL/glu.h>
#include <GL/glut.h>

void Paint(){
    glClear(GL_COLOR_BUFFER_BIT); // 初始化颜色缓冲区
    glColor3f(1, 0, 0); // 设置颜色为红色 (R, G, B)
    glBegin(GL_TRIANGLES); // 三角形
    glVertex2d(-0.8, -0.8); // 顺时针方向指定三顶点坐标,连线
    glVertex2d(0.8, -0.8);
    glVertex2d(0, 0.8);
    glEnd(); // 三角形定义结束
    glFlush();// 线段定义结束
}

int main(int argc, char **argv){
    glutInit(&argc, argv); // 做题不写
    glutInitWindowSize(200, 200); // 程序窗口大小
    glutCreateWindow("红色三角形"); // 窗口标题
    glutDisplayFunc(Paint); // 执行场景绘制函数
    glutMainLoop(); // 开始循环执行 OpenGL 命令
}
T5

题目:请使用OpenGL和GLUT编写一个简单的图形程序,用于显示一个填充的蓝色平行四边形。其中平行四边形的4个顶点分别是(-0.9, -0.4)、(0.4, -0.4)、(0.9, 0.4)和(-0.4, 0.4),程序窗口的大小为(300, 300),标题为“蓝色平行四边形”

#include <GL/glu.h>
#include <GL/glut.h>

void Paint(){
    glClear(GL_COLOR_BUFFER_BIT); // 初始化颜色缓冲区
    glColor3f(0, 0, 1); // 设置颜色为蓝色 (R, G, B)
    glBegin(GL_QUADS); // 四边形
    glVertex2d(-0.9, -0.4); // 顺时针方向指定四顶点坐标,连线
    glVertex2d(0.4, -0.4);
    glVertex2d(0.9, 0.4);
    glVertex2d(-0.4, 0.4);
    glEnd(); // 定义结束
    glFlush();
}

int main(int argc, char **argv){
    glutInit(&argc, argv); // 做题不写
    glutInitWindowSize(200, 200); // 程序窗口大小
    glutCreateWindow("蓝色平行四边形"); // 窗口标题
    glutDisplayFunc(Paint); // 执行场景绘制函数
    glutMainLoop(); // 开始循环执行 OpenGL 命令
}
T6

题目:请使用OpenGL和GLUT编写一个简单的图形程序,用于显示一个填充的紫色梯形。其中梯形的4个顶点分别是(-0.9, -0.4)、(0.4, -0.4)、(0.4, 0.4)和(-0.4, 0.4),程序窗口的大小为(300, 300),标题为“紫色梯形”。

#include <GL/glu.h>
#include <GL/glut.h>

void Paint(){
    glClear(GL_COLOR_BUFFER_BIT); // 初始化颜色缓冲区
    glColor3f(1, 0, 1); // 设置颜色为紫色 (R, G, B)
    glBegin(GL_QUADS); // 四边形
    glVertex2d(-0.9, -0.4); // 顺时针方向指定四顶点坐标,连线
    glVertex2d(0.4, -0.4);
    glVertex2d(0.4, 0.4);
    glVertex2d(-0.4, 0.4);
    glEnd(); // 定义结束
    glFlush();
}

int main(int argc, char **argv){
    glutInit(&argc, argv); // 做题不写
    glutInitWindowSize(200, 200); // 程序窗口大小
    glutCreateWindow("紫色梯形"); // 窗口标题
    glutDisplayFunc(Paint); // 执行场景绘制函数
    glutMainLoop(); // 开始循环执行 OpenGL 命令
}
T29

题目:请使用 OpenGL、GLU 和 GLUT 编写一个显示线框立方体的程序。其中立方体的半径为 1.5 单位,并首先绕(0, 0, 0)~(1, 1, 0)旋转 30 度,然后远移 6.5 单位;观察体规定为:视场角=30 度,宽高比=1,近=1,远=100;程序窗口的大小为(200, 200),标题为“线框立方体”

#include <GL/glu.h>
#include <GL/glut.h>

void Paint(){
    glClear(GL_COLOR_BUFFER_BIT); // 初始化颜色缓冲区

    glLoadIdentity(); // 初始化矩阵
    gluPerspective(30, 1, 1, 100); // 视角

    // 按照题意顺序写的,实际情况下面两行调换才能看到立方体
    glRotated(30, 1, 1, 0); // 绕(0, 0, 0)~(1, 1, 0)旋转 30 度
    glTranslated(0, 0, -6.5); // 远移 6.5 单位

    glutWireCube(1.5); //线框体,半径为 1.5 单位
    glFlush();
}

int main(int argc, char **argv){
    glutInit(&argc, argv); // 做题不写
    glutInitWindowSize(200, 200); // 程序窗口大小
    glutCreateWindow("线框立方体"); // 窗口标题
    glutDisplayFunc(Paint); // 执行场景绘制函数
    glutMainLoop(); // 开始循环执行 OpenGL 命令
}
T30

题目:请使用 OpenGL、GLU 和 GLUT 编写一个三维犹他茶壶程序。其中茶壶的半径为 1 单位,并远移 6.5 单位;观察体规定为:视场角=30 度,宽高比=1,近=1,远=100;程序窗口的大小为(200, 200),标题为“尤他茶壶”

#include <GL/glu.h>
#include <GL/glut.h>

void Paint(){
    glClear(GL_COLOR_BUFFER_BIT); // 初始化颜色缓冲区

    glLoadIdentity(); // 初始化矩阵
    gluPerspective(30, 1, 1, 100); // 视角
    glTranslated(0, 0, -6.5); // 远移 6.5 单位
    glutSolidTeapot(1); // 尤他茶壶,半径为 1 单位
    glFlush();
}

int main(int argc, char **argv){
    glutInit(&argc, argv); // 做题不写
    glutInitWindowSize(200, 200); // 程序窗口大小
    glutCreateWindow("尤他茶壶"); // 窗口标题
    glutDisplayFunc(Paint); // 执行场景绘制函数
    glutMainLoop(); // 开始循环执行 OpenGL 命令
}
T31

题目:请使用 OpenGL 和 GLUT 编写一个显示线框球体的简单图形程序。其中球体的半径为 0.8,经线数为 24,纬线数为 12,并绕 x 轴旋转 30 度,程序窗口的大小为(200, 200),标题为“线框球”

#include <GL/glu.h>
#include <GL/glut.h>

void Paint(){
    glClear(GL_COLOR_BUFFER_BIT); // 初始化颜色缓冲区
    glLoadIdentity(); // 初始化矩阵
    glRotated(30, 1, 0, 0); // 旋转
    glutWireSphere(0.8, 24, 12); // 线框球(半径、经线、纬线)
    glFlush();
}

int main(int argc, char **argv){
    glutInit(&argc, argv); // 做题不写
    glutInitWindowSize(200, 200); // 程序窗口大小
    glutCreateWindow("线框球"); // 窗口标题
    glutDisplayFunc(Paint); // 执行场景绘制函数
    glutMainLoop(); // 开始循环执行 OpenGL 命令
}
T32

题目:请使用 OpenGL 和 GLUT 编写一个显示线框椭球体的简单图形程序。其中椭球体的两极方向为上下方向,左右方向的半径为 0.98,上下方向的半径为 0.49,前后方向的半径为 0.6,经线数为 48,纬线数为 24,使用正投影,裁剪窗口为(-1, -0.5)~(1, 0.5),程序窗口的大小为(400, 200),标题为“线框椭球”

#include <GL/glu.h>
#include <GL/glut.h>

void Paint(){
    glClear(GL_COLOR_BUFFER_BIT); // 初始化颜色缓冲区
    glLoadIdentity(); // 初始化矩阵
    glRotated(-90, 1, 0, 0); // 两级为上下方向
    glScaled(0.98, 0.49, 0.6); // 缩放(左右、上下、前后)
    gluOrtho2D(-1, 1, -0.5, 0.5); // 裁剪窗口

    glutWireSphere(1, 48, 24); // 线框球(半径、经线、纬线)
    glFlush();
}

int main(int argc, char **argv){
    glutInit(&argc, argv); // 做题不写
    glutInitWindowSize(400, 200); // 程序窗口大小
    glutCreateWindow("线框椭球"); // 窗口标题
    glutDisplayFunc(Paint); // 执行场景绘制函数
    glutMainLoop(); // 开始循环执行 OpenGL 命令
}
T33

题目:请使用 OpenGL、GLU 和 GLUT 编写一个三维犹他茶壶程序。其中茶壶的半径为 1 单位,并远移 6.5 单位;观察体规定为:视场角=30 度,宽高比=1,近=1,远=100;程序窗口的大小为(200, 200),标题为“旋转的尤他茶壶”。茶壶绕 z 轴不断旋转,旋转的时间间隔为 25 毫秒,角度间隔为 2 度。注意旋转角度必须限定在 0~360 度以内。

#include <GL/glu.h>
#include <GL/glut.h>

int rot = 0; // 旋转角度

// 思路参考 P89
void Timer(int millis){
    rot = (rot + 2) % 360;
    glutPostRedisplay(); // 场景描绘函数
    glutTimerFunc(millis, Timer, millis); // (间隔毫秒数、函数名、参数值)
}

void Paint(){
    glClear(GL_COLOR_BUFFER_BIT); // 初始化颜色缓冲区

    glLoadIdentity(); // 初始化矩阵
    glRotatef(rot, 0, 0, 1); // 绕 z 轴旋转rot
    glColor3f(1, 1, 1);

    gluPerspective(30, 1, 1, 100); // 视角
    glTranslated(0, 0, -6.5); // 远移 6.5 单位
    glutWireTeapot(1); // 3D茶壶,半径为 1 单位
    glFlush();

}

int main(int argc, char **argv){
    glutInit(&argc, argv); // 做题不写
    glutInitWindowSize(200, 200); // 程序窗口大小
    glutCreateWindow("旋转的尤他茶壶"); // 窗口标题

    glutTimerFunc(25, Timer, 25);
    glutDisplayFunc(Paint); // 执行场景绘制函数
    glutMainLoop(); // 开始循环执行 OpenGL 命令
}
T35

题目:请使用 OpenGL、GLU 和 GLUT 编写一个简单的多视口演示程序。要求:在屏幕窗口左下角的 1/4 部分显示一个红色的填充正三角形;在屏幕窗口右上角的 1/4 部分显示一个绿色的填充正方形;三角形和正方形的左下角顶点坐标值均为(0, 0),右下角顶点坐标值均为(1, 0);裁剪窗口均为(-0.1, -0.1)~(1.1, 1.1);程序窗口的大小为(200, 200),标题为“多视口演示”

#include <GL/glu.h>
#include <GL/glut.h>

// 思路参考 P29
void Triangle(){
    glBegin(GL_TRIANGLES);
    glVertex2d(0, 0);
    glVertex2d(1, 0);
    glVertex2d(0.5, 0.8660);
    glEnd();
}

void Rectangle(){ // 矩形
    glRectf(0, 0, 1, 1); // 对角的坐标 (0,0) & (1, 1)
}

void Paint(){
    int w = glutGet(GLUT_WINDOW_WIDTH);
    int h = glutGet(GLUT_WINDOW_HEIGHT);
    glLoadIdentity(); // 初始化矩阵
    gluOrtho2D(-0.1, 1.1, -0.1, 1.1); // 裁剪窗口

    glClear(GL_COLOR_BUFFER_BIT); // 初始化颜色缓冲区

    glViewport(0, 0, w/2, h/2); // 左下
    glColor3f(1, 0, 0);
    Triangle();

    glViewport(w/2, h/2, w/2, h/2); // 右上
    glColor3f(0, 1, 0);
    Rectangle();

    glFlush();
}

int main(int argc, char **argv){
    glutInit(&argc, argv); // 做题不写
    glutInitWindowSize(200, 200); // 程序窗口大小
    glutCreateWindow("多视口演示"); // 窗口标题
    glutDisplayFunc(Paint); // 执行场景绘制函数
    glutMainLoop(); // 开始循环执行 OpenGL 命令
}

OpenCV

T35

请使用 OpenCV 编写一个简单的程序,用于从当前目录读入并显示一幅彩色图像(例如当前目录中的
lena.jpg)。

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

int main() {
    IplImage *im = cvLoadImage("075.jpg", 1); // 声明 IplImage 指针,载入彩色图像。0是灰色

    if(im == 0) return -1;

    cvShowImage("OpenCV_T35", im); // 显示图像

    while (cvWaitKey(0) != 27){} // 等待Esc

    cvReleaseImage(&im); // 释放图像
    cvDestroyWindow("OpenCV_T35"); // 销毁窗口
}
T36

请使用 OpenCV 编写一个简单的程序,用于从当前目录读入并显示一幅灰度图像(例如当前目录中的
lena.jpg)。

int main() {
    CvMat *mat = cvLoadImageM("075.jpg", 0); // 声明灰度图像
    
    if(mat == 0) return -1;
    
    cvShowImage("OpenCV_T36", mat); // 创建窗口显示图像
    
    while (cvWaitKey(0) != 27) {} // 等待Esc
    
    cvReleaseMat(&mat);
    cvDestroyAllWindows(); // 销毁所有窗口
}
T37

请使用 OpenCV 编写一个简单的程序,该程序首先读入一幅彩色图像(例如当前目录中的 lena.jpg),然
后将这幅彩色图像的 3 个通道分离出来,得到 3 幅灰度图像,最后显示这 3 幅灰度图像。

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

int main() {
    IplImage *img = cvLoadImage("075.jpg", 1); // 载入彩色图像,参数为0载入的是灰色

    if(img == 0) return -1;

    IplImage *redImage = cvCreateImage(cvGetSize(img),IPL_DEPTH_8U,1);
    IplImage *greenImage = cvCreateImage(cvGetSize(img),IPL_DEPTH_8U,1);
    IplImage *blueImage = cvCreateImage(cvGetSize(img),IPL_DEPTH_8U,1);

    // OpenCV采用的色彩模式是 BGR
    cvSplit(img, blueImage, greenImage, redImage, 0);

    cvShowImage("main", img);
    cvShowImage("blue", blueImage);
    cvShowImage("green", greenImage);
    cvShowImage("red", redImage);

    while (cvWaitKey(0) != 27) {}

    cvReleaseImage(&img);
    cvReleaseImage(&redImage);
    cvReleaseImage(&greenImage);
    cvReleaseImage(&blueImage);
    cvDestroyAllWindows();
}
T38

请使用 OpenCV 编写一个简单的程序,该程序从 1 幅彩色图像(使用当前目录中的 lena.jpg)中分离出蓝
色通道,得到 1 幅灰度图像。要求显示源图像和结果图像。

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

int main() {
    IplImage *img = cvLoadImage("075.jpg", 1); // 载入彩色图像

    if(img == 0) return -1;

    IplImage *blueImage = cvCreateImage(cvGetSize(img),IPL_DEPTH_8U,1);

    // OpenCV采用的色彩模式是BGR
    cvSplit(img, blueImage, 0, 0, 0);

    cvShowImage("main", img);
    cvShowImage("blue", blueImage);

    while (cvWaitKey(0) != 27) {}

    cvReleaseImage(&img);
    cvReleaseImage(&blueImage);
    cvDestroyAllWindows();
}
T39

请使用 OpenCV 编写一个简单的程序,该程序首先从一幅真彩色图像(使用当前目录中的 lena.jpg)中选
取一个矩形子集,并用蓝色填充该矩形子集,然后显示图像。其中矩形子集的起始位置为(64, 96),大小为(96, 48)。

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

int main() {
    CvMat *img = cvLoadImageM("075.jpg", 1); // 载入彩色图像
    if(img == 0) return -1;
    CvRect R = {64, 69, 96, 48}; // 起始位置和大小
    CvMat head; // 结果矩阵头
    CvMat *Y = cvGetSubRect(img, &head, R); // 选取矩形子集
    cvSet(Y, CV_RGB(0, 0, 255), NULL); // 矩形子集改为 蓝色:CV_RGB(0, 0, 255)

    cvShowImage("main", img);
    while (cvWaitKey(0) != 27) {}
    cvReleaseMat(&img);
    cvDestroyAllWindows();
}
T40

使用 OpenCV 装入一幅大小至少为 512*512 的真彩色图像,并显示该图像(使用当前目录中的 lena.jpg)。
然后在源图像中指定一个矩形区域(左上顶点和宽高值分别为(128, 256)和(256, 128)的矩形),并在结果图像窗口中显示源图像中被选取的部分。

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

int main() {
    CvMat *img = cvLoadImageM("075.jpg", 1); // 载入彩色图像
    if(img == 0) return -1;
    CvRect R = {128, 256, 256, 128}; // 起始位置和大小
    CvMat head; // 结果矩阵头
    CvMat *Y = cvGetSubRect(img, &head, R); // 选取矩形子集

    cvShowImage("main", Y);// 这里是显示 Y 区域
    while (cvWaitKey(0) != 27) {}
    cvReleaseMat(&img);
    cvDestroyAllWindows();
}
T41 -- 没太理解

使用 OpenCV 编写一个程序,该程序将一幅灰度图像(使用当前目录中的 lena.jpg)的灰度值线性地变换
到范围[0, 255]。要求分别显示源图像和结果图像。

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

int main() {
    CvMat *X = cvLoadImageM("075.jpg", 0); // 载入灰度图像

    // 定义结果图像,与源图像大小相同的双精度矩阵
    CvMat *Y = cvCreateMat(X->rows, X->cols, CV_64F);
    cvScale(X, Y, 1, 0);   // Y(i) = 1 * X + 0 
    cvNormalize(Y, Y, 0, 255, CV_MINMAX, 0);

    cvShowImage("Main", X);
    cvShowImage("Scale", Y);

    while (cvWaitKey(0) != 27) {}

    cvReleaseMat(&X);
    cvReleaseMat(&Y);
    cvDestroyAllWindows();
}
T42

随机生成一幅浮点数灰度图像(大小和亮度都是随机的,大小值位于区间[128, 639]),然后将该图像变
换成亮度是 0~1 的浮点数图像,最后变换成字节图像并显示该图像。

一脸懵逼:opencv随机生成点图像

T46

使用 OpenCV 编写一个演示傅立叶变换和逆变换的程序。该程序首先装入一幅灰度图像并显示该图像(例
如当前目录中的 lena.jpg),然后对该图像进行傅立叶正变换,对得到的结果进行傅立叶逆变换,显示得到的结果以便与原图像进行比对。

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

int main() {
    CvMat *X = cvLoadImageM("075.jpg", 0); // 载入灰度图像

    // 定义结果图像,与源图像大小相同的双精度矩阵
    CvMat *Y = cvCreateMat(X->rows, X->cols, CV_64F);
    cvScale(X, Y, (double)1 / 255, 0);

    cvDFT(Y, Y, CV_DXT_FORWARD, 0);
    cvDFT(Y, Y, CV_DXT_INVERSE_SCALE, 0);

    cvShowImage("Main", X);
    cvShowImage("Dst", Y);

    while (cvWaitKey(0) != 27) {}

    cvReleaseMat(&X);
    cvReleaseMat(&Y);
    cvDestroyAllWindows();
}
T49(复现课本,编译出错)

题目:使用 OpenCV 编写一个演示离散余弦变换和逆变换的程序。该程序首先装入一幅灰度图像并显示该图像(例如当前目录中的 lena.jpg),然后对该图像进行离散余弦正变换,对得到的结果进行离散余弦逆变换,显示得到的结果以便与原图像进行比对。

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

int main() {
    CvMat *src = cvLoadImageM("075.jpg", 0); // 载入灰度图像

    CvMat *X = cvCreateMat(src->rows, src->cols, CV_64F);
    cvScale(src, X, (double)1/255, 0);

    cvDCT(X, X, CV_DXT_FORWARD);

    // 靠近原点的元素改为0
    for (int u = 0; u < src->cols; ++u)
        for (int v = 0; v < src->rows; ++v)
            cvmSet(src, v, u, 0);
    cvDCT(X, X, CV_DXT_INVERSE);

    cvShowImage("Source", src);
    cvShowImage("DCT", X);

    while (cvWaitKey(0) != 27) {}

    cvReleaseMat(&src);
    cvReleaseMat(&X);
    cvDestroyAllWindows();
}
T52、53、54

题目:使用 OpenCV 编写一个程序,该程序对一幅灰度图像(使用当前目录中的 lena-n.jpg)进行一次简单模糊、高斯模糊、中值模糊,要求分别显示源图像和结果图像。其中内核大小分别为 3×3、5x5、3x3。

注意,main函数第一行,和注释部分内核大小,请根据题意做出更改。

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

int main() {
    CvMat *src = cvLoadImageM("075.jpg", 1); // 载入彩色图像,0表示灰色图像
    cvShowImage("Source", src);

    // 结果图像
    CvMat *X = cvCreateMat(src->rows, src->cols, src->type);

    cvSmooth(src, X, CV_BLUR, 3, 3, 0, 0); // 简单,3*3模板
    cvShowImage("CV_BLUR", X);

//    cvSmooth(src, X, CV_GAUSSIAN, 5, 5, 0, 0);// 高斯,5*5模板
//    cvShowImage("CV_GAUSSIAN", X);
//
//    cvSmooth(src, X, CV_MEDIAN, 3, 3, 0, 0); // 中值,3*3模板
//    cvShowImage("CV_MEDIAN", X);

    while (cvWaitKey(0) != 27) {}

    cvReleaseMat(&src);
    cvReleaseMat(&X);
    cvDestroyAllWindows();
}

效果图:1562420178278.png

T55

题目:使用 OpenCV 编写一个程序,该程序对一幅灰度图像(例如当前目录中的 lena.jpg)进行 Sobel 锐化,要求显示(T56:显示原图像和)锐化以后的图像。其中内核大小为 3×3,x 和 y 方向均使用 1 阶差分(T56:使用 1 阶 x 差分,T57:使用Laplace,3*3内核)。

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

int main() {
    CvMat *src = cvLoadImageM("075.jpg", 0); // 载入图像
    if(src == 0)
        return -1;
    
    // cvShowImage("source", src); // T56
    CvMat *tmp = cvCreateMat(src->rows, src->cols, CV_32F);
    cvSobel(src, tmp, 1, 1, 3); // 使用Sober,x,y方向1阶差分,3*3内核
    // cvSobel(src, tmp, 1, 0, 3); // T56 使用Sober,x方向1阶差分,3*3内核
    // cvLaplace(src, tmp, 3); // T57,使用Laplace,3*3内核
    
    // 下三行也可直接 cvShowImage("Sober", tmp);
    CvMat *Y = cvCreateMat(src->rows, src->cols, CV_8U); // 字节图像
    cvConvert(tmp, Y); // 输出图像转化为字节图像
    cvShowImage("Sober", Y);

    while (cvWaitKey(0) != 27) {}

    cvReleaseMat(&Y);
    cvDestroyAllWindows();
}
T61

题目:使用 OpenCV 编写一个程序,该程序对一幅灰度图像(例如当前目录中的 lena.jpg)进行直方图均衡化,要求分别显示源图像和均衡化以后的图像。

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

int main() {
    CvMat *src = cvLoadImageM("075.jpg", 0); // 载入图像
    if(src == 0)
        return -1;
    cvShowImage("source", src);
    cvEqualizeHist(src, src); // 应用直方图均衡化
    cvShowImage("EqualizeHist Image", src);

    while (cvWaitKey(0) != 27) {}
    cvReleaseMat(&src);
    cvDestroyAllWindows();
}
T62

题目:使用 OpenCV 编写一个程序,该程序对一幅灰度图像(例如当前目录中的 lena.jpg)进行二值化变换,要求分别显示源图像和二值化以后的图像。其中二值化阈值为 127,高亮度改为 255。

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

int main() {
    CvMat *X= cvLoadImageM("075.jpg", 0); // 载入图像
    if(X == 0)
        return -1;
    cvShowImage("Source", X);
    cvThreshold(X, X, 127, 255, CV_THRESH_BINARY); // 二值化
    cvShowImage("阈值(127),高度(255)", X);

    while (cvWaitKey(0) != 27) {}
    cvReleaseMat(&X);
    cvDestroyAllWindows();
}
T63

题目:使用 OpenCV 编写一个程序,该程序对一幅灰度图像(例如当前目录中的 lena.jpg)进行 Canny 边缘检测,要求分别显示源图像和检测到的边缘。其中小阈值为 50,大阈值为 150,内核大小为 3。

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

int main() {
    CvMat *X= cvLoadImageM("075.jpg", 0); // 载入图像
    if(X == 0)
        return -1;
    cvShowImage("Source", X);

    cvCanny(X, X, 50, 3 * 50, 3);
    cvShowImage("Canny: 50, 3 * 50", X);

    while (cvWaitKey(0) != 27) {}
    cvReleaseMat(&X);
    cvDestroyAllWindows();
}
]]>
默认分类计算机图形图像,openCV,openGL
<![CDATA[macOS安装Beego]]>https://ariser.cn/index.php/archives/129/

安装Beego

安装Go之后,用以下命令安装或升级Beego:

go get -u github.com/astaxie/beego
go get -u github.com/beego/bee

安装过程一直卡在git的clone阶段:

~  go get -u github.com/astaxie/beego
# cd .; git clone https://github.com/astaxie/beego /xxxxxxx/GoProject/src/github.com/astaxie/beego
正克隆到 '/xxxxxxxx/GoProject/src/github.com/astaxie/beego'...
error: RPC failed; curl 56 LibreSSL SSL_read: SSL_ERROR_SYSCALL, errno 54
fatal: 远端意外挂断了
fatal: 过早的文件结束符(EOF)
fatal: index-pack 失败
package github.com/astaxie/beego: exit status 128

官方教程给出的方案是关闭githttps验证:

git config --global http.sslVerify false

添加环境变量

参考传送门
这里注意的是,PATH路径要更改为:(更改后记得source使生效)

export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

验证安装

命令行bee能执行,即表示安装成功:

Bee is a Fast and Flexible tool for managing your Beego Web Application.

USAGE
    bee command [arguments]

AVAILABLE COMMANDS
###
 #
 #
 ###

Use bee help [command] for more information about a command.

ADDITIONAL HELP TOPICS

建立第一个Beego项目

$ cd $GOPATH/src
$ bee new hello
$ cd hello
$ bee run
______
| ___ \
| |_/ /  ___   ___
| ___ \ / _ \ / _ \
| |_/ /|  __/|  __/
\____/  \___| \___| v1.10.0
2019/06/16 14:55:04 INFO     ▶ 0001 Using 'hello' as 'appname'
2019/06/16 14:55:04 INFO     ▶ 0002 Initializing watcher...
2019/06/16 14:55:05 SUCCESS  ▶ 0003 Built Successfully!
2019/06/16 14:55:05 INFO     ▶ 0004 Restarting 'hello'...
2019/06/16 14:55:05 SUCCESS  ▶ 0005 './hello' is running...
2019/06/16 14:55:05.962 [I] [asm_amd64.s:2337]  http server Running on http://:8080

程序运行起来后,浏览器http://localhost:8080/,见到小蜜蜂即代表运行成功!
屏幕快照 2019-06-16 下午2.57.56.png

]]>
Web开发,macOS,默认分类,GolangWeb开发,macOS,golang
<![CDATA[开箱一个Go语言]]>https://ariser.cn/index.php/archives/125/

前言

一学期一度的课程设计来咯,这次Web课程设计,依旧是各种xxx管理系统,继续拿PHP搞一通很快就搞完了,没啥意思(其实后面如果go搞不下去,可能会->PHP真香)。准备用Beego,目前的认知的是一种为微服务而生的框架。先开箱Go,了解基本语法。

下面是开箱的全过程。

go学习笔记

语言变量

  1. import一个包之后,不能不使用,否则应该写作 import _ "fmt"
  2. 正括号不能写到下一行
  3. 声明了变量,必须要使用,否则会编译错误
  4. ''单引号,指的是rune类型,也就是C中的char。var a = 'a'
  5. Println: 可以打印出字符串,和变量,自带换行。
  6. Printf: 只可以打印出格式化的字符串,可以输出字符串类型的变量,不可以输出整形变量和整形
  7. int、string、bool没有赋值时,分别对应0,,false
  8. fmt.Printf("%v %v %v %q\n", int, float64, bool, string)
  9. := 是声明并赋值,并且系统自动推断类型,不需要var关键字。如d := 100 ,d 不能是已经声明过的
  10. 这种因式分解关键字的写法一般用于声明全局变量
var (
        a int
        b bool
    )
  1. 多变量可以在同一行进行赋值,如:
var a, b int
var c string
a, b, c = 5, 7, "abc"
// 如果没有被调用,可以直接
a, b, c := 5, 7, "abc"
  1. 引用类型写作 (ref)r1,r1就为引用类型的变量
  2. 空白标识符:
func main() {
  _,numb,strs := numbers() //只获取函数返回值的后两个
  fmt.Println(numb,strs)
}

//一个可以返回多个值的函数
func numbers()(int,int,string){
  a , b , c := 1 , 2 , "str"
  return a,b,c
}

语言常量

可以理解为静态变量
常量的定义格式:

const identifier [type] = value
const b string = "abc" // 显式
const b = "abc" // 隐式

常量还可以用作枚举:

const (
    Unknown = 0
    Female = 1
    Male = 2
)

iota

iota,特殊常量,可以认为是一个可以被编译器修改的常量。
iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:

const (
    a = iota
    b
    c
)

牛掰但是还没理解的用法:传送门,看最后几行

远算符

加减乘除什么的都一样用
位运算符号,参见菜鸟教程
同C,有取地址符号&,和指针变量*

键盘输入

fmt.Scan(&b)

条件语句

if

不写括号

if a > 0 {
    if a == 3 {
        ///
    }
    ///
} else {
    ///
}

switch

默认case后面自动执行break,
使用fallthrough,会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true传送门的最后。

var grade string = "B"
var marks int = 90

switch marks {
  case 90: 
    grade = "A"
    fallthrough
  case 80: grade = "B"
  case 50,60,70 : grade = "C"
  default: grade = "D"  
}

用于判断某个 interface 变量中实际存储的变量类型

var x interface{}
     
switch i := x.(type) {
  case nil:   
     fmt.Printf(" x 的类型 :%T",i)                
  case int:   
     fmt.Printf("x 是 int 型")                       
  case float64:
     fmt.Printf("x 是 float64 型")           
  case func(int) float64:
     fmt.Printf("x 是 func(int) 型")                      
  case bool, string:
     fmt.Printf("x 是 bool 或 string 型" )       
  default:
     fmt.Printf("未知型")     
}  
// 结果为:x 的类型 :<nil>

select

直接看详细:传送门

循环语句

看实例


var b int = 15
var a int

// for循环不加括号   
for a := 0; a < 10; a++ {
  fmt.Printf("a 的值为: %d\n", a)
}

// 相当于while
for a < b {
  a++
  fmt.Printf("a 的值为: %d\n", a)
}

numbers := [6]int{1, 2, 3, 5} 
// 相当于foreach,有点没理解
for i,x:= range numbers {
  fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
}   

还有break、continue、goto,直接传送门

函数

  1. 至少得有一哥main()函数
  2. Go 语言标准库提供了多种可动用的内置的函数。例如,len() 函数可以接受不同类型参数并返回该类型的长度。如果我们传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。

函数格式如下:

func function_name( [parameter list] ) [return_types] {
   函数体
}
// 函数名(参数列表)返回值类型

实例:

/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
   /* 声明局部变量 */
   var result int

   if (num1 > num2) {
      result = num1
   } else {
      result = num2
   }
   return result 
}

可以返回多个值

func swap(x, y string) (string, string) {
   return y, x
}

func main() {
   a, b := swap("Google", "Runoob")
   fmt.Println(a, b)
}

参数传递和函数用法

值传递引用传递(和C语言一样,直接改变地址对应的值)
函数作为另外一个函数的实参闭包方法

全局变量

传送门

数组

定义格式:

var balance [10] float32

初始化:

var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

未知大小数组:

var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
var salary float32 = balance[9]

多维数组

Go 语言向函数传递数组

传送门

指针、结构体、Range、Map、接口、并发

传送门

Go中没有明确指出面向对象概念,借助结构体struct实现,用interface实现接口,具体传送门

成员方法(类里面的方法)的实现:

type Person struct {
  name string
  age int
}

person := Person{"mike",18}
fmt.Println(person)

func (person *Person) showInfo() {
    fmt.Printf("My name is %s , age is %d ",person.name,person.age)
}

func (person *Person) setAge(age int) {
    person.age = age
}

person := Person{"mike",18}
person.showInfo()
person.setAge(20)
fmt,Println(person)

在关键字func和函数名之间加上一对小括号,写上与之关联的结构体类型指针类型变量名推荐写成指针类型的原因是,函数传递的形参不是指针类型的话,无法覆盖实参。【直接copy的】

继承和多态,见传送门

总结

GO还是比较优雅的,有种用Python的简介化封装C语言的感觉,其余的后面用多了再说。尝试一种新语言或框架,有一种开箱一个新的电子产品的感觉。

上面的基础内容,应该掌握,就可以开始搞事了。(我是花了四小时进行学习和整理文档的)

唉,考研狗还瞎折腾这些,有种出轨的感觉。。。(正确姿势应该是用是用到烂的php框架一天搞完课设,然后一天实验报告,剩下时间全部刷高数)

]]>
Web安全,默认分类,GolangWeb开发,golang
<![CDATA[心灵有约技术实录]]>https://ariser.cn/index.php/archives/138/

PSY简介

“心灵有约”是一款基于微信企业号的Web应用,用于在校师生进行心理咨询的在线预约和受理,详见:心理有约-使用手册。系统已通过学校网络中心的安全评估和性能测试,于五月份在校企业号上线。截止目前已有1800+师生注册使用。

系统已申请一项软件著作权。

2193971889.png

技术架构简述

系统采用前后端分离模式进行开发。

前端使用的是基于Bootstrap的开源UI模板AdminLTEV3版本,配合其它前端工具;

后端框架为开源接口框架PhalApi,好处是可以快速且规范地写业务并发布接口,主要借助此框架完成业务处理的分层和接口封装。

由于后端框架API层只负责接口发布,于是自行结合签名认证PHP-NoCSRF自行造了一个接口验证的轮子改良NoCSRF实现对PHP后端接口的安全验证

身份认证基于企业微信进行开发,可以实现外来身份识别和校内用户自动注册。

运行于CentOS上的LAMP

身份认证

身份认证主要由身份控制脚本和用户登录、注册接口组成。踩过无数坑之后造的轮子。

流程描述为:访问页面 - 获取微信用户身份 - 判断用户身份 - 查询、插入数据库 - 返回用户信息 - 重定向到访问页面

image-20190814162137728.png

用户身份控制脚本封装为了Header类,只能由统一入口进行调用,且用户只能通过统一入口访问应用,通过URL后缀参数定向到不同页面。这样设置的原因:

  1. 将用户身份验证模块剥离出来,业务界面专门负责内容渲染,而统一入口配合Header脚本管控用户身份。
  2. 微信授权中code的获取,需要通过页面访问来实现,而回调的也是一串带有很多参数的URL,这些参数会带来很多意想不到的bug,产生的bug也是驱动“设置统一入口”的原因。设置统一入口后,身份验证成功后便重定向到业务页面。
<?php
/**
 * 唯一入口
 * Created by PhpStorm.
 * User: Aris
 * Date: 2019-03-03
 * Time: 21:41
 */
require_once"../vendor/autoload.php";
use App\Common\Header;

// http://xxxxx/psy/pages/index.php?href=main
if(isset($_GET['href'])){
    $href = $_GET['href'];
}else{
    $href = 'submit';
}

$t_url='http://'.$_SERVER['HTTP_HOST'].'/psy/pages/index?href='.$href;
$header = new Header();
$header->index($t_url);

header("location:$href");

Header.php

<?php
/**
 * Created by PhpStorm.
 * User: Aris
 * Date: 2018/12/19
 * Time: 上午8:38
 */

namespace App\Common;

use PhalApi\CUrl;

class Header{

    /**
     * 开启session,获取用户信息
     * @param $t_url
     */
    public function index($t_url){
        session_start();

        if(!isset($_SESSION["adminPsy"]) || $_SESSION["adminPsy"] != true){
            $respond = $this->getArray($t_url);
            $_SESSION = $respond;
        }
    }

    /**
     * 获取用户数组
     * @param $t_url
     * @return mixed
     */
    public function getArray($t_url){
        //获取code
        if(!isset($_GET['code'])) {
            $redirect_uri = urlencode($t_url);//页面地址
            echo $t_url;
            //拼接地址
            $url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxxxxxxxxx&redirect_uri=".$redirect_uri."&response_type=code&scope=snsapi_userinfo&state=1#wechat_redirect";
            //重新访问
            header("location:".$url);
            exit;
        }else {
            $code = $_GET['code'];
        }
//        echo "code = ".$code."</br>";
        
        $respond = '';
        //带code请求登陆接口
        $post_url = 'http://'.$_SERVER['HTTP_HOST'].'/psy/public/?s=Mobile_Login/Index';
        try{
            $curl = new CUrl(2);
            $rs = $curl->post($post_url, array( 'code' => $code ), 3000);
            $respond = json_decode($rs, true);
        } catch (\PhalApi_Exception_InternalServerError $ex){}
        return $respond['data'];
    }
}

微信开发

这一部分按照微信官方给出的授权流程进行操作,使用PhalApi封装的CURL请求类PhalApiCUrl进行接口的请求。主要流程如下:

p1.png

流程和普通微信公众号开发一样,只是将一些参数换了名称,比如OpenID在这里叫做UserID。其余的一些配置信息需要从应用管理员控制台获取到。

达到的效果:组织内用户点开,后台调用接口进行注册,直接使用。组织外用户拒绝访问、毕业学生拒绝访问、微信以外渠道不能访问。

每个模块流程为:检验参数合法 - 拼接URL - 调用$curl->get($URL, 3000);

/**
     * 获取用户信息
     * @param $access_token
     * @param $UserID
     * @return array|bool|string
     */
protected function getUserInfo($access_token, $UserID){
        $curl = new CUrl();
        $userInfoURL = 'https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token='.$access_token.'&userid='.$UserID;
        if (isset($UserID)) {
            $UserInfo = $curl->get($userInfoURL, 3000);
            $UserInfo= (array)json_decode($UserInfo, true);
            return $UserInfo;
        }
        return false;
    }

接口安全

系统后端所有功能都以接口的形式发布,所有的方法都只能通过调用接口来实现。所以接口的安全性是重中之重。

最开始直接使用的签名验证。后面接触到了防范CSRF攻击,它是借助csrf_token来实现,但是原方法似乎只能对“一个页面访问一个接口”起到保护作用,于是进行改良。再后来就想着能否将二者结合起来,将这个token也用于验签过程中的随机字符串,于是就又开始了造轮子改良NoCSRF实现对PHP后端接口的安全验证。大致流程如下:

p4.png

其它安全

模板布局

这里是参考ThinkPHP以及其它带有视图渲染的框架的做法,进行了代码精简和统一性控制。

通过将前端页面的公共部分进行提取(比如手机页面的引用的css、菜单栏等),封装为header.phpfooter.php等,然后在每个业务页面的头部和尾部直接include,中间部分就直接写content部分了。这样一来直接更改header.php中的菜单栏,即可对所有页面生效。

<?php include 'common/header.php';?>
  <div class="content">
      <!-- ///// -->
  </div>
<?php include 'common/footer.php'?>

业务处理

框架本身分层分得十分好,模型层、业务层、接口层,分别处理份内的事情。数据库由NotORM接管,写起来也十分舒服,不过只限于一些简单的操作,复杂的查询或其它业务语句,则要写原生的,不过要按照官方文档的要求写SQL避免被注入。

这一篇其实是众多文档之一,还有很多写给用户的、写给后面做运维同学的、测试文档等,给后面的同学交接本系统应该不会很悲剧(注释还是写得比较多的)。。

优化

  • 优化了静态资源的CDN
  • 给常查询的字段添加了索引
  • 修改了所有select *,改为了需要的字段
  • null默认值改为了0
  • Apache改成了Nginx,确实快了一些

后期计划

  • 尝试使用Redis进行数据缓存(试过用Redis存储session)
  • 尝试使用Docker部署
  • Golang复现
]]>
PHP,Web开发,Web安全,微信开发,Linux,JavaScript,默认分类
<![CDATA[数据库常用语句及优化思想]]>https://ariser.cn/index.php/archives/334/

基础操作

  • 登录:

    • -h:主机名
    • -u:用户名
    • -p:使用密码登录
  • 管理:

    • use DB;
    • show database
    • show tables
    • show columns form 表
    • show index form 表
    • exit
  • 数据库操作:

    • 创建:CREATE DATABASE 数据库名;
    • 删除:drop database 数据库名;
  • 数据表操作:

    • 创建:CREATE TABLE table_name ('列名' 类型 属性, 列名 类型, 主键('xxx'));

      CREATE TABLE IF NOT EXISTS `runoob_tbl`(
         `runoob_id` INT UNSIGNED AUTO_INCREMENT, // 自增
         `runoob_title` VARCHAR(100) NOT NULL, // 非空
         `runoob_author` VARCHAR(40) NOT NULL,
         `submission_date` DATE,
         PRIMARY KEY ( `runoob_id` ) // 主键
      )ENGINE=InnoDB DEFAULT CHARSET=utf8;
    • 删除:DROP TABLE table_name ;
    • 插入:INSERT INTO xxx (field1, field2,...fieldN) VALUES(field1, field2,...,fieldN)
    • 查询:

      SELECT column_name, column_name
      FROM table_name
      [WHERE Clause]
      [LIMIT N][ OFFSET M]
      • LIMIT 4读取四条;LIMIT 2, 4:第三条起读取四条。分页LIMIT startRow,pageSize;
      • limit 4 offset 9:返回四行,从第十行开始
    • 更新:UPDATE table_name SET field1=new-value1, field2=new-value2 [WHERE Clause]
    • 删除:DELETE FROM table_name WHERE age<20
    • Like:条件, %字符来表示任意字符,类似正则*'%COM'
    • UNION:连接两个查询的集合,删除重复数据。

      • SELECT A, B FROM tables WHERE XX
        UNION [ALL | DISTINCT]
        SELECT A, C FROM tables WHERE XX;
      • DISTINCT默认就是删除重复,加不加无用。ALL返回所有结果集,包含重复的
    • ORDER BY: ORDER BY XXX DESC,默认升序。
    • GROUP BY:按字段分组,并统计数目。GROUP BY name得到:name | count数目
    • 连接:

      • INNER JOIN(内连接, 或等值连接):获取两个表中字段匹配关系的记录。
      • LEFT JOIN(左连接):获取左表所有记录,即使右表没有对应匹配的记录。
      • RIGHT JOIN(右连接): 与 LEFT JOIN 相反,用于获取右表所有记录,即使左表没有对应匹配的记录。
    • Alter:修改数据表名或字段。

      • ALTER TABLE testalter_tbl DROP i; 删除表的i字段
      • ALTER TABLE testalter_tbl ADD i INT; 表添加字段,默认是末尾
      • ALTER TABLE testalter_tbl ADD i INT FIRST;第一列添加
      • ALTER TABLE testalter_tbl ADD i INT AFTER c;c列之后添加
      • ALTER TABLE testalter_tbl MODIFY c CHAR(10);改类型
      • ALTER TABLE testalter_tbl CHANGE i j BIGINT;改i为j,类型为BIGINT
      • ALTER TABLE testalter_tbl RENAME TO alter_tbl; 改表名

InnoDB和MySAM

  • InnoDB和MySAM
  • 操作性:

    • InnoDB具有事务,支持四个事务隔离级别。

      • 适用于大量INSERT或UPDATE操作。
      • 不支持全文索引,新版本支持。
      • 支持外键
    • MyISAM管理非事务表

      • 它提供高速存储和检索,以及全文搜索能力。
      • 如果应用中需要执行大量的SELECT查询,那么MyISAM是更好的选择
      • 不支持事务、外键
  • 存储:

    • MyISAM在磁盘上存储成三个文件。表定义 .frm,数据文件.MYD, 索引文件.MYI。跨平台转移麻烦
    • InnoDB:空间数据文件和它的日志文件,InnoDB 表的大小只受限于操作系统文件的大小
  • 索引:

    • InnoDB(索引组织表):能缓存索引,也能缓存数据,必须得有主键
    • MyISAM(堆组织表):只能缓存索引。非聚类
  • InnoDB 在做SELECT的时候,要维护缓存数据和索引和其余的,慢一些。MyISAM只缓存索引块
  • MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。

    • B+树搜索算法搜索索引-取出data值-以此为地址读取数据记录
    • 主索引要求key是唯一的,而辅助索引的key可以重复
  • InnoDB也使用B+Tree作为索引结构,数据文件本身就是索引文件。

    • 叶节点data域就是数据记录,称之聚类索引
    • 本身要按主键聚类,所以必须要主键(设置自增主键),没有的话分裂维持特性会十分低效。没指定的话会自动选择合适的或自动生成一个隐含字段。长度为6的长整型
    • 辅助索引data存储的主键值

索引

  • 索引可以看作一张表,指向实体表记录
  • 唯一索引,保证每行数据的唯一性
  • 加大检索速度,特别是分组和排序
  • 加速表和表之间的连接速度
  • 缺点:创建和维护要耗时(增删时要动态维护),占用物理空间
  • 总结:会提高查询速度,但是DML会变慢(更新维护索引)
  • 语法:

    • CREATE INDEX indexName ON mytable(username(length));
    • ALTER table tableName ADD INDEX indexName(columnName)
    • DROP INDEX [indexName] ON mytable;
  • 添加主键命令:

    mysql> ALTER TABLE testalter_tbl MODIFY i INT NOT NULL;
    mysql> ALTER TABLE testalter_tbl ADD PRIMARY KEY (i);

导出数据

mysqldump -u root -p dbs_name table_name > dump.sql [--all-databases]
mysql -u root -p dbs_name < dump.sql

create database adc;
use abc;
set names ytf8;
source /xx/xx/xx/abc.sql

范式

  • 第一范式:当关系模式R的所有属性都不能在分解为更基本的数据单位时,称R是满足第一范式的,简记为1NF。满足第一范式是关系模式规范化的最低要求,否则,将有很多基本操作在这样的关系模式中实现不了。
  • 第二范式:如果关系模式R满足第一范式,并且R得所有非主属性都完全依赖于R的每一个候选关键属性,称R满足第二范式,简记为2NF。
  • 第三范式:设R是一个满足第一范式条件的关系模式,X是R的任意属性集,如果X非传递依赖于R的任意一个候选关键字,称R满足第三范式,简记为3NF.
  • 注:关系实质上是一张二维表,其中每一行是一个元组,每一列是一个属性
  • 第四范式:要求把同一表内的多对多关系删除。
  • 第五范式:从最终结构重新建立原始结构。
  • BC范式(BCNF):符合3NF,并且,主属性不依赖于主属性。若关系模式R属于第一范式,且每个属性都不传递依赖于键码,则R属于BC范式。

优化方法

  • 微博整理

    • null值容易引发灾难,id is null而不是id=null
    • union代替or
  • 数据库设计
  1. 避免全表扫描,在where 及 order by 涉及的列上建立索引
  2. 避免在 where 子句中对字段进行 null 值判断,引擎会放弃索引而使用全局扫描。可以考虑把null默认值设为0
  3. 一个表的索引数最好不要超过6个,在一些不常用到的的列不需要索引。因为会降低插入、修改的速度
  4. 避免更新索引数据,若该列频繁更新,考虑索引的必要性
  5. 字符会一一比较,数字只需要比较一次。所以能用数值尽量不要用字符
  6. 尽可能的使用 varchar/nvarchar 代替 char/nchar,变长字段存储空间小,可以节省存储空间
  7. 尽量少使用临时表
  • SQL语句
  1. 避免在Where使用 != 或 <>,以及or,否则引擎放弃使用索引而进行全表扫描。

    select id from t where num=10 or num=20
    select id from t where num=10 union all select id from t where num=20
  2. 能用 between 就不要用 in,not in
  3. where 子句中使用参数,也会导致全表扫描。强制加上索引

    select id from t where num=@num
    select id from t with(index(索引名)) where num=@num
  4. 避免在 where 子句中对字段进行表达式操作:=”左边进行函数、算术运算或其他表达式运算

    select id from t where num/2=100 
    select id from t where num=100*2
  5. 具体的字段列表代替“*”,不要返回用不到的任何字段。
  6. 避免使用游标,超过一万行就考虑改写
  7. 避免大事务操作
]]>
数据库
<![CDATA[心灵有约-使用手册]]>https://ariser.cn/index.php/archives/78/

应用简介

心灵有约-心理咨询在线预约系统,是用于校心理健康教育中心进行心理咨询的在线受理平台。本应用依托于微信企业号,面向于所有在校师生。

预约流程

提交预约 - 等待受理 - 受理通过 - 现场咨询

使用简介

进入系统

打开企业号,点击心灵有约即可进入。
系统会自动识别身份,并与教务系统对接,在本应用内生成您的个人信息和联系方式,因此请确认信息并修改以便我们能联系到您。
2193971889.png

在线预约

根据自己的情况合理选择咨询师(预约咨询的时间默认为下周次),并在备注栏简要说明问题,点击提交申请
4231888369.png

我的预约

提交预约后可以在我的预约界面查看预约申请。对正在受理状态的预约可以进行取消预约
等待后台受理之后,会在企业号为您推送一条消息,点击详情,按照提示信息前往心理健康教育中心进行心理咨询。
2844283580.png

个人中心

用于修改个人联系方式,默认您的联系方式来自教务系统,若有偏差请及时修改,确认是有效的联系方式,以便我们能及时联系到您!
1964597894.png

在线咨询

在线咨询仅回复一般心理问题,需进行深入心理咨询,建议进行在线预约。
4290845518.png
在线咨询会唤起QQ,请出现提示后选择

使用指南和关于我们

介绍在线预约流程和常见问题,以及校心理健康教育中心的简介,欢迎扫码关注湖科大心理在线
136442089.png

常见问题

  • 问题1:我遇到了较为紧急的心理障碍需要排解。答:请直接致电校心理教育健康中心:0731-58291687。
  • 问题2: 申请的预约是在线进行心理咨询吗?答:不是,本应用是用于"现场咨询"的在线预约。
  • 问题3:如果我临时有事情,可以取消预约吗?答:在"正在受理"的状态下可以进行"取消预约"。
  • 问题4: 我提交的预约信息会被其他人看到吗?答:不会,您提交的预约只有心理健康教育中心的咨询师可以看到,隐私信息也由系统全程进行加密保护。
  • 问题5: 我提交了预约,可以再申请以提高成功率吗?答:不行,咨询师资源有限,“正在受理”和“受理通过”情况下不可再提交新的预约。
  • 问题6: 我转专业了,学院信息不一致答:如有学院信息不一致情况,请在"在线咨询"回复:张三-更改学院-XXX学院。
  • 问题7: 我提交的申请为什么会被退回呢?答:提交的申请不规范或咨询师临时有事情,可以重新提交申请。
  • 问题8: 为什么我把这个分享给同学不能直接使用呢?答:本应用唯一入口是"企业号 ➔ 心灵有约"。

问题反馈

使用过程如果有任何问题语言不规范,请发送邮件到i@ariser.cn1136206244联系,董。

]]>
PHP,Web开发,Web安全,数据结构,微信开发,Linux,JavaScript,默认分类
<![CDATA[Webp优化多图网站加载]]>https://ariser.cn/index.php/archives/44/

事由:近期把博客从WordPress转到了Typecho,使用了material主题。该主题文章显示可以选择随机图片的样式,样式如下:

屏幕快照 2019-04-13 下午6.04.53.png

将自己准备好的图片命名为material-x.png(x为数字),替换Themes/material/im/random下的图片。如果图片数目很多,请将控制台-外观-设置外观中的随机缩略图数量设置为你文件下图片的数目。

这时候问题来了

  1. 图片数量过多,重命名过于麻烦。
  2. 准备的图片文件过大网页加载奇慢无比,压缩(降低分辨率)后又会变模糊。

网页完全加载完用了30秒,且大部分时间是在等待png格式的图片加载。

QQ20190413-182755.png

于是又想到了webp格式,766k的jpg格式图片转化成webp只有377k,大大节省了加载时间,且清晰度下降可以忽略不计。且支持动图。
QQ20190413-182516.png

写了Python脚本来实现批量图片格式转换以及自动命名为material-x.webp样式。

  • 创建jpgwebp文件夹,将自己选好的图片放入到jpg,运行脚本即可自动转换。(其他格式自己该脚本里面的代码)
import PIL.Image
import os
i=1
path = "jpg/"
savepath = "webp/"
filelist = os.listdir(path)
for file in filelist:
    im = PIL.Image.open(path+filelist[i])
    filename = 'material-'+str(i)
    print(filename)
    im.save(savepath+filename+'.webp')
    i=i+1

另外还需将主题默认png格式更改为webp,编辑usr/themes/Meterial/functions.php210240行中的png改为webp

更改之后,图片质量无损的情况下,加载速度大为提升。

主题地址:Material

]]>
PythonWeb开发,Python
<![CDATA[从防护角度浅谈服务器安全和网站安全]]>https://ariser.cn/index.php/archives/24/

弱密码、常用密码、防止社工

  1. 任何时候、任何场景,请不要使用弱口令。
  2. 社工库是真真实实存在的,当你QQ、微博、服务器、邮箱、各种站点的会员,长期使用的过程中,不管密码有多复杂,有可能已经被泄露,而被存入黑客所谓的社工库(亲身经历,一个有社工库的朋友拿我的邮箱直接查出了我的最常用的一个密码)。这个泄露即使在用户安全上网的情况下也会存在,所注册的站点一旦被脱裤子,这些密码就直接泄露出去(优酷、B站等大互联网公司都有被脱库的事件)。
  3. 检测是否存在泄露的密码:传送门
  4. 还有屡见不鲜的钓鱼网站,伪造输入QQ账号密码登录界面,群邮件是重灾区(考研群发送"来参加复试"的邮件蠕虫式传播);在手机QQ或电脑QQ登录的情况下,凡事官方登录页面都会有一键点击登录或者扫码登录。这样被盗号的,总结一个字"傻!"。

服务器安全

  • 环境:CentOS 7
  • 工具:XShell(Windows)FinalShell(macOS)

我们知道,SSH远程登录端口默认为22,任何人都可以通过对应端口尝试登录你的服务器,针对服务器主机的攻击,一般都是机器扫描批量扫描爆破的
(宝塔面板提示)
1

因此,解决措施有如下:

  • 关闭root账户远程登录(CentOS)

    1. 创建新的账户,设置ssh登录:传送门
    2. sudo vim /etc/ssh/sshd_config
    3. /PermitRootLogin->Enter,查找
    4. 去掉#PermitRootLogin yes注释,或改为PermitRootLogin no
    5. 重启ssh服务/etc/rc.d/init.d/sshd restart
  • 禁用Ping、不用时关闭SSH登录
    宝塔面板设置:
    2
  • 更改默认SSH端口号(暂时缓解,大部分端口扫描工具可以直接扫出来)
  • 安装悬镜、云锁、安全狗等安全软件(只安装一个)
  • 使用复杂密码
  • 开启防火墙,只放行要用到的端口

网站安全

  1. 隐藏后台管理入口
  2. 除了update/cache等少数目录,其它所有目录都给只读权限
  3. 在nginx/apache站点配置中限制除入口目录及资源目录以外的所有目录的访问权限
  4. 若网站程序支持,尽量使用php5.4以上的版本
  5. 如非必要,不要给站点创建FTP,使用完就删除或停用FTP帐户
  6. 如非必要,不要对外开放3306端口,并隐藏好phpmyadmin位置,最好设个访问密码
  7. 开启SSL(HTTPS)
  8. 保护好网站源码及数据库备份,请不要将数据库备份及网站源码包等敏感数据放在站点根目录
  9. 使用Nginx时,可在宝塔面板中开启WAF防火墙,可有效防止绝大多数web攻击

代码托管平台提交项目前删除配置信息

  1. GitHub上提交开源项目,一定注意要将运维WIKI和数据库配置信息等敏感数据删除掉再上传。
  2. 如果已经上传,删除后重新提交Push是没用的!!!,因为每次Push都会有文件更改记录:

因此建议直接删除仓库里的项目重新提交。

]]>
Web开发,Web安全,Linux,默认分类
<![CDATA[PHP设计模式-框架底层各种方法的实现]]>https://ariser.cn/index.php/archives/347/框架用起来固然方便,帮你封装好了各种方法,只负责按照给出的示例调用即可,这样其实不利于对一个语言的了解。这几天在B站看到了一个合集的视频,讲述PHP的各种设计模式,以及在MVC模型中的实现,感觉似曾相识但又从没动手写过这些设计思想。看完才发现,由面向对象扩展出来的还有这么多种设计模式。这里做一个学习记录。

三种基本设计模式

  1. 工厂模式:工程方法或类生成对象,而不是在代码中直接new
  2. 单例模式:使某个类的对象只允许被创建一次
  3. 注册模式:全局共享和交换对象

工厂模式

普通使用

$db = new \Work\Database();

工厂模式

// DBFactory.php
namespace Work;

interface DB{
    public funtion connect();
}

// 产品1
class _mysqli implements DB{
    public funtion connect(){
        echo 'mysqli';
    }
}
// 产品2
class _pdo implements DB{
    public funtion connect(){
        echo 'pdo';
    }
}

class DBFactory {
    static public function Factory($class_name){
        return new $class_name;
    }
    
    // 一般是下面这种写法
    static public function Factory($class_name){
        switch($class_name){
            case '_pdo':
                return new _pdo();
                break;
            case 'mysqli':
                return new _mysqli();
                break;
        }
    }
}
$db = \Work\DBFactory::Factory('_pdo');
$db->connect();

好处在于如果$db = new Database();在很多地方被new,如果Database()名称或请求参数被改变,那么很多地方都要做出改变。有了工厂模式,只需要在工厂里面进行改变。

根据参数返回不同的产品

比如:项目中多处实例化这个类,后面发现类名不合适或要添加构造函数参数,那就要到处改了

使用工厂模式,用参数获取不同的类,只需要改工厂里面的类,调用方不用改

单例模式

  • 避免到处被new而消耗资源,常用于数据库操作
  • 用单例全局控制某些配置信息

确保 一个类只有一个实例,并且对外部提供这个全局实例访问入口

  1. 一个类只有一个对象
  2. 私有化构造方法:防止被new
  3. 私有化克隆方法:方式被 clone
  4. 构造实例,new自己
// Database.php
namespace Work;

class Database {
    // 静态变量私有化
    private static $db;
    
    // 私有构造方法防止被 new
    private function __construct() {
    }
    
    // 私有克隆方法,防止被克隆
    private function __clone(){
    }
    
    // 静态方法,用来被实例化,对外的接口
    public static function getInstance(){
        if(self::$db){
            return self::$db;
        } else {
            self::$db = new self();
            return self::$db;
        }
    }
}

实现了单例模式

无论调用多少次,创建数据库操作只会new()一次。

// index.php
$db = Work\Database::getInstance();
$db = Work\Database::getInstance();
$db = Work\Database::getInstance();
$db = Work\Database::getInstance();

工厂内部也要改:

namespace Work;

class Factory {
    static function createDatabase(){
        $db = Database::getInstance();
        return $db;
    }
}

注册树模式

上述两种方法都有缺点,都需要调用工厂或类。

把对象注册到注册树上,那么在任何一个地方都能调用。

// Register.php
namespace Work;

class Register {
    protected static $objects;
    
    static function set($alias, $object){
        self::$objects[$alias] = $object;
    }
    
    function _unset($alias){
        unset(self::$objects[$alias]);
    }
}

在工厂里面把对象映射上去:(只在这构造一次)

// Factory.php
namespace Work;
class Factory {
    static function createDatabase(){
        $db = Database::getInstance();
        Register::set('db1', $db);
        return $db;
    }
}

使用的时候,不需要调用工厂或单例,直接在注册器上拿;

$db = \Work\Register::get('db1');

适配器模式

  1. 将截然不同的函数借口封装成统一的API
  2. 比如:mysql、mysqli、Pdo 三种可以用适配器统一改成一致。还有Cache适配器,比如Memcahe、Redis、File、apc缓存函数统一成一致。

声明一个接口,约定统一API方式

// Wrok/Database.php
namespace Work;

interface IDatabase{
    function connect($host, $user, $password, $dbname);
    function query($sql);
    function close();
}

分别创建三种适配器的实例,分别实现接口内的方法:

<?php
// Work/Database/MySQL.php
namespace Work\Database;
use Work\IDatabase;

class MySQL implements IDatabase {
    protected $conn;
    
    function connect($host, $user, $password, $dbname){
        $conn = mysql_connect($host, $user, $password);
        mysql_select_db($dbname, $conn);
        $this->conn = $conn;
    }
    function query($sql){
        $res = mysql_query($sql, $this->conn);
        return $res;
    }
    function close(){
        mysql_close($this->conn);
    }
}

实现MySQLi:

<?php
// Work/Database/MySQLi.php
namespace Work\Database;

use Work\IDatabase;
class MySQLi implements IDatabase{
    protected $conn;
    
    function connect($host, $user, $password, $dbname){
        $conn = mysqli_connect($host, $user, $password, $dbname);
        $this->conn = $conn;
    }
    function query($sql){
        return mysqli_query($this->conn, $sql);
    }
    function close(){
        mysqli_close($this->conn);
    }
}

实现PDO:

<?php
// Work/Database/PDO.php
namespace Work\Database;
use Work\IDatabase;

class PDO implements IDatabase{
    protected $conn;
    
    function connect($host, $user, $password, $dbname){
        $conn = mysqli_connect($host, $user, $password, $dbname);
        $this->conn = $conn;
    }
    function query($sql){
        return mysqli_query($this->conn, $sql);
    }
    function close(){
        mysqli_close($this->conn);
    }
}

实现:三种模式直接切换

$db = new \Work\Database\MySQL();
// $db = new \Work\Database\PDO();
// $db = new \Work\Database\MySQLi();
$db->connect('127.0.0.1', 'root', 'xxx', 'test');
$db->query('show database');
$db->close();

策略模式

  1. 将一组特定行为和算法封装成类,以适应某些特定的上下文环境
  2. 例如:电商网站,正对男性女性用户要各自跳转到不同的商品类目,并且所有广告位展示不同的广告

实现:https://www.bilibili.com/video/av19567303/?p=17

观察者模式

一个对象发生改变时,依赖它的对象全部会收到通知并自动更新。实现了低耦合,非侵入式的通知与更新机制。

实现:https://www.bilibili.com/video/av19567303/?p=21

原型模式

和工厂模式类似,都用来创建对象。不同在于,原型模式先创建好一个原型对象,再通过CLone原型对象来创建新的对象。免去了创建对象时的初始化操作。适用于大对象的创建(大对象每new一次会消耗很大,只需要内存拷贝)。

实现:https://www.bilibili.com/video/av19567303/?p=22

装饰器模式

动态添加类的功能。传统方法是写一个子类继承它,并重实现类。装饰器的话只需要再运行的时候添加一个装饰器对象即可实现。

实现:https://www.bilibili.com/video/av19567303/?p=23

迭代器模式

不需要了解内部实现的前提下,遍历一个聚合对象的内部元素。相对于传统方法,迭代器可以隐藏遍历元素所需的操作。

实现:https://www.bilibili.com/video/av19567303/?p=24

代理模式

客户端 和实体之间建立一个代理对象proxy,客户端对实体进行操作会委派给代理对象,隐藏实体的具体实现细节。

还可以与业务代码分离,部署到另外的服务器中。

https://www.bilibili.com/video/av19567303/?p=25

面向对象设计基本准则
  1. 单一职责:一个类只需要做好一件事情
  2. 开放封闭:一个类应该是可以扩展的,而不可修改的
  3. 依赖倒置:一个类,不应该依赖另一个类。每个类对另外一个类是可替代的。
  4. 配置化:尽可能使用配置,而不是硬编码
  5. 面向接口编程:只需要关心接口,不需要面向实现

MVC

模型(Model):数据和存储的封装

视图(view):展现层的封装,比如Web系统中的模板文件

控制器(Controller):逻辑层的封装,业务代码

各种设计模式在MVC里面的使用

https://www.bilibili.com/video/av19567303/?p=30

]]>
PHP
<![CDATA[Laravel - Mac下搭建环境_Docker部署]]>https://ariser.cn/index.php/archives/409/

环境搭建

社区Wiki给的环境搭建有四种方法:Laravel 安装和开发环境:Mac 开发环境布置

  • Laravel Homestead —— 官方 Ubuntu 虚拟机
  • Laravel Valet —— 官方的 Mac 集成环境
  • Laradock —— 专为 Laravel 优化的 PHP Docker 虚拟环境方案
  • 其他方案

最后想着顺便学习一波Docker,所以选择了第三种Laradock,参考的是这一篇文章:在 Mac/Windows 系统中使用 Laradock 搭建基于 Docker 的 Laravel 开发环境

安装Docker

这一个UP主发布整理的Docker简介比较容易理解。给你讲的 Docker 实操课 #01 Docker 课程介绍

  • Docker 基于 Go 语言开发,是一个基于 LXC 技术之上构建的 Container 容器引擎。
  • 使用场景:

    • Web 应用的自动化打包和发布;
    • 自动化测试和持续集成、发布;
    • 在服务型环境中部署和调整数据库或其他的后台应用;
    • 从头编译或者扩展现有的 OpenShift 或 Cloud Foundry 平台来搭建自己的 PaaS 环境。
安装Docker,切换国内源

直接使用Homebrew安装Docker

brew cask install docker

也可以直接下载安装包 Docker for Mac Edge

切换国内源:任务栏Docker图标 -> Perferences -> Daemon -> Registry mirrors。

在列表中填写加速器地址即可http://hub-mirror.c.163.com。修改完成之后,点击 Apply & Restart 按钮

常用命令:
docker images : 列出本地镜像
docker pull : 从镜像仓库中拉取或者更新指定镜像

docker run :创建一个新的容器并运行一个命令
-d: 后台运行容器,并返回容器ID
-p: 端口映射,格式为:主机(宿主)端口:容器端口
--name="nginx-lb": 为容器指定一个名称
-v:目录映射,格式为:主机目录:容器目录

docker rm :删除一个或多个容器
docker start :启动一个或多少已经被停止的容器
docker stop :停止一个运行中的容器
docker kill :杀掉一个运行中的容器(强制)
docker restart :重启容器
docker port :列出指定的容器的端口映射,或者查找将PRIVATE_PORT NAT到面向公众的端口。

docker logs : 获取容器的日志
-f : 跟踪日志输出
--since :显示某个开始时间的所有日志
-t : 显示时间戳
--tail :仅列出最新N条容器日志

docker exec -i -t  mynginx /bin/bash:在容器mynginx中开启一个交互模式的终端,即通过SSH协议进入容器

docker ps : 列出容器
-a :显示所有的容器,包括未运行的。

docker cp:拷贝主机docker cp /www/runoob 96f7f14e99ab:/www/

安装Laravel

  1. 克隆项目到本地

    git clone https://github.com/Laradock/laradock.git
  2. 重命名env-example.env

    cd laradock
    cp env-example .env
  3. 运行容器,安装 nginx mysql redis workspace

    docker-compose up -d nginx mysql redis workspace
>Docker-Compose项目是Docker官方的开源项目,负责实现对Docker容器集群的快速编排。Docker-Compose将所管理的容器分为三层,分别是工程(project),服务(service)以及容器(container)。

第一次运行时间比较漫长,因为需要安装。且某些包需要`Docker`账户验证,需要番强出去注册,然后点击`Docker`图标登录。

顺利跑完进度条后,可能会存在端口被占用的问题,最后会有提示:
ERROR: for nginx  Cannot start service nginx: driver failed programming external connectivity on endpoint laradock_nginx_1 (xxxx): Error starting userland proxy: Bind for 0.0.0.0:80: unexpected error (Failure EADDRINUSE)
查询端口对应进程`PID`,`kill`掉所有进程:(`lsof`需要在`sudo`下执行)

lsof -i:80
kill -9 PID
其实你会发现直接通过`kill`是关不了自带的http服务的,根源还是要关闭`apachectl`服务(`macOS`自带`apache`服务)

关闭后,`lsof -i:80`会发现80端口不再占用。再次运行容器

docker-compose up -d nginx mysql redis workspace
# 相关参数可以看上面的常见命令
全部运行
laradock_mysql_1 is up-to-date
laradock_redis_1 is up-to-date
laradock_docker-in-docker_1 is up-to-date
laradock_workspace_1 is up-to-date
laradock_php-fpm_1 is up-to-date
Starting laradock_nginx_1 ... done
  1. 打开 .env添加如下配置:

    DB_HOST=mysql
    REDIS_HOST=redis
    QUEUE_HOST=beanstalkd

创建Laravel应用

  1. laradock父级目录创建和它同级的 wwwroot,cd进去,运行命令创建一个新的laravel应用,会发现出现了一个 blog 的文件夹

    composer create-project laravel/laravel blog --prefer-dist
  2. 编辑配置项 laradock/.env

    APP_CODE_PATH_HOST=../wwwroot/
相当于为 `wwwroot`与Docker的 /var/www 目录建立了软连接,然后在 `laradock/nginx/sites`下增加 `blog.conf`的配置,设置虚拟域名 `blog.test`
server {

    listen 80;
    listen [::]:80;

    server_name blog.test;
    root /var/www/blog/public;
    index index.php index.html index.htm;

    location / {
         try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        try_files $uri /index.php =404;
        fastcgi_pass php-upstream;
        fastcgi_index index.php;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        #fixes timeouts
        fastcgi_read_timeout 600;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }

    location /.well-known/acme-challenge/ {
        root /var/www/letsencrypt/;
        log_not_found off;
    }
}
  1. /etc/host解析域名,添加 127.0.0.1 blog.test
  2. 重启Docker的Nginx (需要到laradock目录下)

    docker-compose up -d nginx
  3. 浏览器访问应用: http://blog.test

    有可能会抛出文件权限错误无法正常运行,到 wwwroot/blog/下设置两个权限

    chmod -R 777 storage
    chmod -R 777 bootstrap/cache
顺便复习下 chmod 和 chown
- chomod 修改文件和文件夹`读写执行属性`  `chmod 777 hh.c` 
    - 可写 w=4 、可读 r=2 、可执行 x=1 
- chown 修改文件和文件夹的`用户和用户组属性`  `chown -R root:www /tmp/xxx `
- 文件权限字段 `drwxr-xr-x` `用户、组、其他人`
    - 第一段:样例中字母“d”,表示文件所在文件夹
    - 第二段:样例中字符串“rwx”,表示文件全部者对此文件的操作权限
    - 第三段。样例中字符串“r-x”,表示文件全部者所在组对些文件的操作权限
    - 第四段。样例中字符串“r-x”,表示除2、3两种外的不论什么人对此文件的操作权限

刷新浏览器,见到以下界面,证明安装成功!

屏幕快照 2019-09-04 下午8.48.16.png

至此,Laravel环境搭建、创建一个 Laraval应用的流程完成。

如果要创建多个应用,就是在 wwwroot目录下创建应用,然后在 laradock/nginx/sites创建配置文件,解析域名。

感兴趣的还可以尝试使用 Vagrant(Homestead) 搭建,另外也可以使用MacOS自带的 Valet 传送门

]]>
PHP,Web开发PHP,Laravel
<![CDATA[Hackintosh_Envy13_10.13.6-10.14.5]]>https://ariser.cn/index.php/archives/3/

Hackintosh_Envy13_10.13.6-10.14.5

前言:社区共同开发成果,希望用于个人DIY和技术交流,不得用于商业用途,淘宝贩子还请绕道!

为防止不良TB店家直接盗用,此处不公开EFI。需要完整且最新版,请页面最下方的交流群免费获取、一起交流、参与贡献!(群内有已开发好的惠普其它机型)

简书,Github,码云

进群切勿当伸手党,请先自己逛 黑果小兵远景论坛 ,学习基本安装流程之后自己动手,在群里讨论关键性问题。

完美适用于Hp Envy13 2017 ad1xxx

目前适配到10.14.5,如果因升级系统造成的模块出问题,请重建缓存后重启(使用Kext Utility)。

LZ的Envy13 2017配置为i5 8250u + uhd620 + 8G + 256G + Bcm94352z(网卡),惠普其它型号,可以进群寻找EFI、讨论及共同开发完美版本(群号在最下面)。

亮度快捷键

  1. 外接一个键盘
  2. 偏好设置 - 键盘 - 快捷键 - 显示器 - 用机械键盘设置亮度升降 F2 和 F3
  3. 笔记本调节亮度(Fn 配合 F2 F3)

开启HIDPI(系统原生缩放和显示更细腻)

$ sh -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"

优秀Mac资源网站:

软件推荐

  1. 功能增强工具,自己看图看名称
    image
  2. 神器:鼠标悬浮预览 + 拖拽全屏/窗口
    image
  3. Memory Cleaner:定时清理运行内存

    • General: 1.开机自启 2.隐藏窗口;其余全部关闭
    • Advanced: 勾选Auto Clean,设置6秒清理
    • 关闭 Notifications 里所有选项
  4. Markdown编辑器:MWeb
  5. Others:
    image

以上软件在资源网站里面都能找到。

声卡

声卡ID用的3,可以驱动下面两个喇叭,声音大一些,四个喇叭同时驱动还在寻找方法
有了Hotpatch的话,改config里面的ID是没用的,要改 /Volumes/EFI/EFI/CLOVER/ACPI/patched 下的 SSDT-Config.aml`
SSDT-HDEF.aml`对应位置的ID,可用的还有13,28
image

目前还存在的问题

  1. Windows非正常关机,再引导到MacOS后会长时间读条,强制关机再开机即可
  2. 不同BIOS版本出的问题不一样,有的睡眠后不出声音,有的蓝牙和Wi-Fi失效

交流群

再次重申,交流群只回复关键问题和提供部分机型EFI,不负责一步步教你怎么安装,请先到 黑果小兵远景论坛 学习基本安装步骤后再加群!建议问题解决后对群内大佬说声谢谢以示尊重。

]]>
Hackintosh
<![CDATA[改良NoCSRF实现对PHP后端接口的安全验证]]>https://ariser.cn/index.php/archives/4/

改良NoCSRF实现对PHP后端接口的安全验证

自己造的轮子,用于对前后端分离中后端接口的安全加固,如果有缺陷,还请指出,共同讨论改良!

改良和改造NoCSRF,实现对PhalAPI接口框架等前后端分离架构接口的安全加密认证。

不想看分析思路的可以直接跳到“实现过程”及上传的源码,参照进行部署。

目录:

  • NoCSRF的介绍
  • 配置到框架(以单次请求为示例)
  • 多次请求的处理
  • 解决方案
  • 实现过程
  • 结语

NoCSRF

国外大神开发的一个包,用于防范Web页面中的CSRF攻击。

代码一共有120行,思路很清晰,有兴趣可以进行拜读NoCSRF.php。

思路类似于常见的接口签名的实现:

  1. 请求头IP进行SHA1后,与20位随机码及时间戳连接,最后进行Base64处理。
  2. 每次请求接口前,生成上述$token存储到Session
  3. 携带$token请求接口。
  4. 后台验证时候逐步进行:

    • Session$token存在性检查
    • $_POST数组中$token存在性检查
    • 请求来源检查(请求头IP进行SHA1,与$token中的值进行对比)
    • 验证Session$_POST中的$token是否相同
    • 验证该$token是否过期(比对时间戳)
  5. 验证通过后,执行接口操作,否则抛出异常。
  6. 销毁$token

只要$token生成并存储的位置选择合理(每次页面加载前,PHP网页头部),基本不存在伪造的可能。因为$token生成时就放入了Session数组当中,存储在服务器硬盘Redis等缓冲区中,同时$token作为表单请求,后台将二者进行多重验证。

后面会介绍到,这个包只适用于一个页面对后端只有一次接口调用,多次请求需要进行改良。

配置到框架(以单次请求为示例)

配置前参见官网简单请求的示例:(PHP) NoCSRF

  1. 在框架命名空间中注册:
// nocsrf.php放入到/src/App/Common/
<?php
    namespace App\Common;
    ///。。。
    class NOCSRF{
    
    }
?>
  1. 页面头部生成Token:
<?php
    require_once("../vendor/autoload.php");//自动加载类
    use App\Common\NoCSRF;
    
    session_start();
    $token = NoCSRF::generate('csrf_token');
?>
  1. 表单携带Token:
<form name="csrf_form" action="#" method="post">
   <input type="hidden" name="csrf_token" value="<?php echo $token; ?>">
...Other form inputs...
    <input type="submit" value="Send form">
</form>

如果是Ajax,直接放入到变量当中,记得加上引号: var token = '<?php echo $token; ?>';

  • 后端验证:(对于每一个需要验证的接口,在构造函数内执行,./src/App/Api/xxx.php)
<?php

namespace App\Api;
use PhalApi\Api;

use App\Common\NoCSRF;
use PhalApi\Exception;

class Login extends Api{
    public function __construct(){
        session_start();

        try {
            // Run CSRF check, on POST data, in exception mode, with a validity of 10 minutes, in one-time mode.
            NoCSRF::check( $MainKey, $_POST, true, 60*10, false );
            // form parsing, DB inserts, etc.
        }
        catch ( Exception $e ) {
            exit('Need token!');
            // CSRF attack detected
        }
    } 

    public function getRules(){
    //.....
    }
    //...Other functions...
}
?>

以上内容针对:页面加载一次只请求后台一个接口的情形,比如登录。


多次请求的处理

对于一个页面同时请求多个接口,上述显然不适合。因为页面每次加载只会生成一个$token,而这个$token用于验证后,就会被后台销毁掉,同时请求的其他接口就会失效,而抛出Need Toekn!

思路1:(不可行)

Ajax请求接口的时候再<?php echo NoCSRF::generate('csrf_token');?>

比如下方例子,理论上可行,有请求就生成token,但实际上只有最后一次生成的token有效。因为PHP网页也算作脚本,页面每次刷新,页面内所有的PHP代码都会自动执行,所以前方的token在后方的token生成后被销毁。

function f1(){
    $.ajax({
            url: "xxxxx",
            type: "POST",
            data: {
                    'csrf_token': '<?php echo NoCSRF::generate('csrf_token');?>'
            },
            success: function(res, status, xhr) {
                    console.log(res);
            },
    })
}

function f2(){
    $.ajax({
            url: "xxxxx",
            type: "POST",
            data: {
                    'csrf_token': '<?php echo NoCSRF::generate('csrf_token');?>'
            },        
            success: function(res, status, xhr) {
                    console.log(res);
            },
    })
}
思路2:(可行但漏洞显而易见)

生成token单独作为接口发布,每次需要就先请求再获取。

于是有了以下方案:

  • 生成token的接口./src/App/Api/Token.php
<?php

namespace App\Api;

use PhalApi\Api;
use App\Common\NoCSRF;
use PhalApi\Exception;

class Token extends Api{
    public function getRules(){
        return array(
            'index' => array(),
        );
    }

    public function index(){
        session_start();
        return NoCSRF::generate('csrf_token');
    }
}
  • Ajax调取接口,封装成函数
function getCSRF(){
    let csrf_token = "";
    $.ajax({
            type: "GET",
            cache: false,
            async: false,
            url: "/xxx/xxx/?s=Token/Index",//Tokenj接口的URL
            success: function(res) {
                    csrf_token = res.data;
            }, error: function(XMLHttpRequest, textStatus, errorThrown) {
                    console.log(XMLHttpRequest.status);
                    console.log(XMLHttpRequest.readyState);
                    console.log(textStatus);
                    console.log(errorThrown);
                    csrf_token = "";
            }
    });
    return csrf_token;
}
  • 每次需要就执行:
function refresh(){
    $.ajax({
            url: "xxxxx",
            type: "POST",
            data: {
                    'csrf_token': getCSRF(),
                    'otherData' : 'xxx',
            },        
            success: function(res, status, xhr) {
                    console.log(res);
            },
    })
}

后面发现,直接用Postman请求/xxx/xxx/?s=Token/Index,获取到token,再携带这个token请求其他接口,依然能访问成功。

思考发现,这时的token仍然是在服务器端生成,无状态的HTTP请求直接拿过去,再反回来请求,依然是可行,只起到了验证时效性验证的功能,token与客户端没有唯一性联系,这种方案脱离了NoCSRF包本身的设计思路。


解决方案:

每次生成token的过程'NoCSRF::generate('csrf_token');',其中的'csrf_token'是自定义的,那么不妨把这个key利用起来,使之成为唯一且动态变化的值。

普通请求

在每个页面首部生成Token1,作为后面接口生成的tokentoken_key,请求是下面的样子:

改良后的请求

由于Token1是在页面首部(自身脚本,相当于与客户端绑定)生成的,不存在被伪造的可能 (原因见文章第一部分的介绍) ,故身份具有唯一性,拥有token的网页才可以访问接口。


实现过程:

注册接口: 每次页面加载会生成Token1,并请求此接口验证身份,当作token_key
token接口: 请求此接口会得到token_key: token2(上图)样式的Token用于业务接口的验证。
常规接口: 业务接口,比如“获取列表”。

  • 每个页面生成Token1并前往注册,注册成功Token1采纳,否则为空:(存储在session中)
<?php
    require_once("../vendor/autoload.php");
    use App\Common\NoCSRF;

    session_start();
    $token = NoCSRF::generate('csrf_token');
    $_SESSION['token_key'] = $token;//token_key或者Token1
?>

<html>
<body>
<script>
    var token_key = "";
    $.ajax({
            url: "/xxx/public/?s=Token/Login",//身份注册接口
            type: "POST",
            cache: false,
            async: false,
            data:{
                    "csrf_token": "<?php echo $token?>",
            },
            success: function(res) {
                    token_key = "<?php echo $token?>";
            }, error: function(error) {
                    console.log(error);
                    token_key = "";
            }
    });
</script>
</body>
</body>
  • 注册接口:./src/App/Api/Token.php(这里的key仍然是‘csrf_token’)
public function Login(){//页面头部的注册
    session_start();
    try {
        NoCSRF::check( 'csrf_token', $_POST, true, 60*10, false );
    }
    catch ( Exception $e ) {
        unset($_SESSION['token_key']);//验证不通过就销毁
        exit('Need token!');
    }
}
  • 拥有token_key后获取组合token: (此处开始,tokentoken_key作为键值)
        // ./src/App/Api/Token.php
    public function index(){
        session_start();
        $token_key = $_SESSION['token_key'];
        //generate函数的参数不再是'csrf_token'而是$token_key
        return NoCSRF::generate($token_key);
    }

    //Ajax请求Token,这一步无变化
    function getCSRF(){
            let csrf_token = "";
            $.ajax({
                    type: "GET",
                    cache: false,
                    async: false,
                    url: "/xxx/xxx/?s=Token/Index",             
                    success: function(res) {
                            csrf_token = res.data;
                    }, error: function(error) {
                            console.log(error);
                            csrf_token = "";
                    }
            });
            return csrf_token;
    }

    //请求业务接口,这里需要将Token1/token_key作为key,其中token_key就是页面首部生成,通过身份注册的
    <script>
        let json_data = {
                'data1' : 'xxx',
        };
        //注意,变量作为key传输必须用下方写法,不能用上面json格式写法,否则key直接为'token_key'.
        json_data[token_key] = getCSRF();

        $.ajax({
                url: "/xxx/xxx/?s=Order/GetList",
                type: "POST",
                data: json_data,        
                success: function(res, status, xhr) {
                        console.log(res);
                        //
                },
        })
     </script>
  • 业务接口验证(对于每一个需要验证的接口,在构造函数内执行,./src/App/Api/Order.php):
public function __construct(){
    session_start();

    //验证$_SESSION中是否存在'token_key'
    if(!isset($_SESSION['token_key'])){
        exit('Need token!');
    }
    $token_key = $_SESSION['token_key'];

    //注意下方check函数的第一个参数不再是'csrf_token'而是$token_key
    try {
        NoCSRF::check( $token_key, $_POST, true, 60*10, false );
    }
    catch ( Exception $e ) {
        exit('Need token!');
    }    
}

public function getRules(){
     //...
}

/// ...Other functions...

完结

至此,整个从前端请求和后端接口验证过程结束。至于如何部署到Phalapi框架或其他框架里面,相信看完整个过程就可以上手,也可以直接查看上传的示例。

本方案只针对采用前后端分离框架开发的微服务项目中,接口安全验证的防护。Web开发中涉及到方方面面的安全性问题:明文传输、数据库明文存储、XSS、渗透、社工等,要想让项目固若金汤,开发过程中都勇于去面对这些问题,寻找方案进行加固。

《鸟哥的Linux私房菜-服务器架设篇》和《大型网站技术架构》推荐阅读
]]>
PHP,Web开发,Web安全,JavaScriptHackintosh
<![CDATA[2.3-线性表的链式表示]]>https://ariser.cn/index.php/archives/106/

单链表的实现和基本操作

// 单链表的操作

#include <iostream>
#include <stdio.h>
#include <stdlib.h>

typedef int ElemType;

typedef struct LNode{// 定义单链表节点类型
    ElemType data;        // 数据域
    struct LNode *next;    //指针域
}LNode, *LinkList;

// 头插法
LinkList List_HeadInsert(LinkList &L){
    LNode *s;
    int x;

    L = (LinkList)malloc(sizeof(LNode));    // 创建头节点
    L->next = NULL;                            //初始化为空链表

    scanf("%d", &x);
    while( x != 999 ){
        s = (LNode*)malloc(sizeof(LNode));    // 创建新结点(分配空间,把地址赋值给 s)
        s->data = x;
        s->next = L->next;        // L 为头指针
        L->next = s;
        scanf("%d", &x);
    }
    return L;
}

// 尾插法
LinkList List_TailInsert(LinkList &L){
    int x;
    L = (LinkList)malloc(sizeof(LNode));
    LNode *s;
    LNode *r = L;    // 表尾指针

    scanf("%d", &x);
    while( x != 999 ){
        s = (LNode *)malloc(sizeof(LNode));
        s->data = x;        // 插入结点赋值
        r->next = s;        // 新结点插入表中
        r = s;                // 地址赋给尾结点,r 指向新的表尾结点,作为下一次插入的临时区
        scanf("%d", &x);
    }
    r->next = NULL;            // 尾结点指针置空
    return L;
}

// 按照序号查找结点值(带头结点)
LNode *GetElem(LinkList L, int i){
    int j = 1;                // 计数,初始值为1
    LNode *p = L->next;        // 头结点赋值给 p
    if(i == 0){
        return L;
    }
    if(i < 1){
        return NULL;
    }
    while (p && j < i){        // 从第1个开始查找
        p = p->next;
        j++;
    }
    return p;                // 返回第 i 个结点的指针,i 大于表长, p = NULL, 直接返回p即可
}

// 删除第 i 个结点
bool List_Delete(LinkList L, int i){
    LinkList p = GetElem(L, i - 1); // 查找删除位置的前驱结点
    if(p == NULL){
        return false;
    }
    LinkList q;
    q = p->next;            // q 指向被删除结点
    p->next = q->next;        // *q从链中断开 相当于 p->next = p->next->next;,但不能这么写,因为要释放qDLinkList
    free(q);                // 释放结点储存空间
    return true;
}

// 位置插入
bool ListFrontInsert(LinkList L, int i, ElemType e){
    // 创建新结点
    LinkList s = (LNode*)malloc(sizeof(LNode));
    s->data = e; 

    LinkList p = GetElem(L, i-1); // 插入位置前驱结点
    if(NULL == p){
        return false;
    }
    s->next = p->next; // 新结点指向当前位置对应结点
    p->next = s;    // 插入位置前驱结点指向当前插入结点
    return true;
}

// 打印链表
void List_Print(LinkList L){
    L = L->next;
    while (L != NULL){
        printf("%3d", L->data);
        L = L->next;
    }
    printf("\n");
}


int main(void){
    LinkList L;
    LinkList search;

    List_TailInsert(L);
    List_Print(L);

    search = GetElem(L, 2);
    if(search != NULL){
        printf("%d\n", search->data);
    }

    printf("位置插入:\n");
    ListFrontInsert(L, 2, 99);
    List_Print(L);
    printf("位置删除:\n");
    List_Delete(L, 4);
    List_Print(L);
    
    return 0;
}

双链表的实现和基本操作

// 双链表
// 结论:画示意图后参照写步骤
#include <iostream>
#include <stdio.h>
#include <stdlib.h>

using namespace std;

typedef int ElemType;

typedef struct DNode{    // 定义双链表节点类型
    ElemType data;        // 数据域
    struct DNode *prior, *next;    // 前驱和后继指针
}DNode, *DLinklist;

// 头插法
DLinklist DList_head_insert(DLinklist &DL){
    DNode *s;
    int x;

    DL = (DLinklist)malloc(sizeof(DNode));// 创建空链表
    DL->next = NULL;
    DL->prior = NULL;
    scanf("%d", &x);

    while(x != 999){
        s = (DNode*)malloc(sizeof(DNode));// 创建新节点
        s->data = x; // 数据域赋值

        s->next = DL->next; // *s 插入到 *DL 之后         ---1
        if(DL->next != NULL){
            DL->next->prior = s;// 第一个节点指向插入的 s     ---2
        }
        s->prior = DL;                                //    ---3

        DL->next = s;                                //    ---4 , 1、2必须在4之前
        scanf("%d", &x);
    }
    return DL;
}

// 尾插法
DLinklist DList_tail_insert(DLinklist &DL){
    int x;
    DL = (DLinklist)malloc(sizeof(DNode));
    DNode *s, *r = DL;

    DL->prior = NULL;
    scanf("%d",&x);
    while(x != 999){
        s = (DNode*)malloc(sizeof(DNode));
        s->data = x;
        r->next = s;
        s->prior = r;
        r = s;
        scanf("%d",&x);
    }
    r->next = NULL;
    return DL;
}

// 取元素
DNode *GetElem(DLinklist DL, int i){
    int j = 1;
    DNode *p = DL->next;

    if(i == 0){
        return DL;
    }
    if(i < 1){
        return NULL;
    }

    while(p && j < i){
        p = p->next;
        j++;
    }
    return p;
}

// 元素头部插入 (p 之后插入 s)
bool DListFrontInsert(DLinklist DL, int i, ElemType e){
    DLinklist p = GetElem(DL, i-1);
    if(p == NULL){
        return false;
    }
    DLinklist s = (DNode*)malloc(sizeof(DNode));
    s->data = e;

    s->next = p->next;
    p->next->prior = s;
    s->prior = p;
    p->next = s;
    return true;
}

// 元素删除
bool DListDelete(DLinklist DL, int i){
    DLinklist p = GetElem(DL, i-1);
    if(p == NULL){
        return false;
    }
    DLinklist q;
    q = p->next;
    if(q == NULL){
        return false;
    }
    p->next = q->next;
    if(q->next != NULL){
        q->next->prior = p;
    }
    free(q);                // 需要释放内存
    return true;
}

// 打印
void PrintDList(DLinklist DL){
    DL = DL->next;
    while(DL != NULL){
        printf("%3d\n", DL->data);
        DL = DL->next;
    }
    printf("\n");
}

int main(void){
    DLinklist DL;
    DLinklist search;

    // DList_head_insert(DL);
    DList_tail_insert(DL);
    printf("尾插法结果:\n");
    PrintDList(DL);

    search = GetElem(DL, 2);
    if(search != NULL){
        printf("序号查找:");
        printf("%d\n\n", search->data);
    }

    DListFrontInsert(DL, 3, 666);
    printf("3位置插入:\n");
    PrintDList(DL);


    DListDelete(DL, 3);
    printf("3位置删除:\n");
    PrintDList(DL);

    return 0;
}

课后代码题目

]]>
数据结构数据结构
<![CDATA[2.2—线性表的顺序表示]]>https://ariser.cn/index.php/archives/100/

顺序表的实现和基本操作

#include <iostream>
#include <stdio.h>
using namespace std;

// 取地址符的意义:https://blog.csdn.net/u011723466/article/details/27109249
// 没有取地址符,就变化不了实际的值。所以 PrintfList 直接 SqList L。
// 要改变 L ,当 元素 = e, 这时写 f(SqList &L, ElemType e){}
// 要改变 L,当 元素 = e,更改为 x,并读取x,这时写 f(SqList &L, ElemType &e){}
#define MaxSize 50

typedef int ElemType;

// 静态分配,长度就是 MaxSize
typedef struct{
    ElemType data[MaxSize];
    int length;
} SqList;

// 动态分配,长度见下方的调用
#define InitSize 100    // 表长度的初始定义
typedef struct {
    ElemType *data;
    int MaxSize_1, length;
}SeqList;

// e 插入到 L 中的第 i 个位置
bool ListInsert(SqList &L, int i, ElemType e){
    if(i < 1 || i > L.length + 1){// 判断 i 的范围是否有效
        //!!! 之所以可以在 length + 1插入,是因为:
        //尾部插入一个,顺序表仍然符合:连续性
        printf("插入的位置超出范围!\n");
        return false;
    }    
        
    if(L.length >= MaxSize){// 储存空间已满,不能插入
        printf("存储空间满!\n");
        return false;
    }        
        
    for(int j = L.length; j >= i; j--){    // i 元素及之后的元素后移
        L.data[j] = L.data[j - 1];
    }
    L.data[i - 1] = e;        //位置 i 放入e
    L.length++;            // 线性表长度加1
    
    return true;
}

// 删除 L 中的 第 i 个元素
bool ListDelete(SqList &L, int i, ElemType &e){
    if(i < 1 || i > L.length){
        return false;
    }

    e = L.data[i-1];        // 被删除元素赋给e
    // 因为data是数组,所以 i-1
    for(int j = i; j < L.length; j++){
        L.data[j-1] = L.data[j];    // 元素前移,当前等于后方的元素
    }

    L.length --;    //表长度减1
    return true;
}

// 查找 L 中值为 e 的第一个元素
int LocateElem(SqList L, ElemType e){
    for(int i = 0; i < L.length; i ++){
        if(L.data[i] == e){
            return i+1; // 返回位序为 i + 1
        }
    }
    return 0;
}

void PrintfList(SqList L){
    for(int i = 0; i < L.length; i++){
        printf("%4d\n", L.data[i]);
    }
    printf("\n");
}


int main(void){
    SqList L;
    ElemType del;

    L.data[0] = 1;
    L.data[1] = 2;
    L.data[2] = 3;
    L.data[3] = 4;
    L.length = 5;

    if(ListInsert(L, 2, 10)){
        printf("插入成功!\n");
        PrintfList(L);
    }else{
        printf("插入失败!\n");
    }

    if(ListDelete(L, 2, del)){
        printf("删除成功!\n");
        printf("删除的元素为: %d\n", del);
        PrintfList(L);
    }else{
        printf("删除失败!\n");
    }

    int ret = LocateElem(L, 3);
    if(ret){
        printf("查找成功!\n");
        printf("元素位置为:%d\n", ret);
        PrintfList(L);
    }else{
        printf("查找失败!\n");
    }

    return 0;
}

课后代码题

2.2.3-1

#include <iostream>
#include <stdio.h>
using namespace std;

#define MaxSize 50

typedef int ElemType;

// 静态分配,长度就是 MaxSize
typedef struct{
    ElemType data[MaxSize];
    int length;
} SqList;

bool Del_Min(SqList &L, ElemType &e){
    // e 为元素值
    if(L.length == 0){
        printf("顺序表为空!\n");
        return false;
    }

    e = L.data[0];
    int e_i = 0;    // 次序码
    for(int i = 0; i < L.length; i++){
        if(L.data[i] < e){
            e = L.data[i];
            e_i = i;
        }
    }

    L.data[e_i] = L.data[e_i + 1];
    L.length --;
    
    return true;
}

void PrintfList(SqList L){
    for(int i = 0; i < L.length; i++){
        printf("%4d\n", L.data[i]);
    }
    printf("\n");
}

int main(void){
    SqList L;
    ElemType del;

    L.data[0] = 1;
    L.data[1] = 2;
    L.data[2] = 3;
    L.data[3] = 4;
    L.data[4] = -5;
    L.length = 5;

    int ret = Del_Min(L, del);
    if(ret){
        printf("删除的最小元素为:%d\n", del);
        PrintfList(L);
    }else{
        printf("操作失败!\n");
    }
    return 0;
}

2.2.3-2

/**
 * 翻转线性表,空间复杂度为1
 */
#include <iostream>
#include <stdio.h>
using namespace std;
#define MaxSize 50
typedef int ElemType;

typedef struct{
    ElemType data[MaxSize];
    int length;
} SqList;

void PrintfList(SqList L){
    for(int i = 0; i < L.length; i++){
        printf("%4d", L.data[i]);
    }
    printf("\n");
}

void ReverseList(SqList &L){

    // 自己实现,空间复杂度为 L 的长度,其实没必要
    // SqList L2 = L;
    // for(int i = 0; i < L.length / 2; i++){
    //     L.data[i] = L.data[L.length - i -1];
    // }

    // for(int j = 0; j < L2.length / 2; j++){
    //     L.data[L.length - j -1] = L2.data[j];
    // }
    
    // 答案写法,提前记录再放到后面,只用一个缓冲区
    ElemType temp; 
    for(int i = 0; i < L.length / 2; i++){
        temp = L.data[i];
        L.data[i] = L.data[L.length - i -1];
        L.data[L.length - i -1] = temp;
    }
    
}

int main(void){
    SqList L;
    ElemType del;

    L.data[0] = 1;
    L.data[1] = 2;
    L.data[2] = 3;
    L.data[3] = 4;
    L.data[4] = 5;
    L.length = 5;

    PrintfList(L);
    ReverseList(L);
    PrintfList(L);

    return 0;
}

2.2.3-3

/**
 * 删除线性表中所有值为 x 的数据元素。时间复杂度O(n),空间复杂度O(1)
 */
#include <iostream>
#include <stdio.h>
using namespace std;

#define MaxSize 50
typedef int ElemType;

typedef struct{
    ElemType data[MaxSize];
    int length;
} SqList;

void PrintfList(SqList L){
    for(int i = 0; i < L.length; i++){
        printf("%4d", L.data[i]);
    }
    printf("\n");
}

void Delete_Elem_x(SqList &L, ElemType e){

    // 方法1, 不等于 e 的元素放入新数组
    int k = 0; //不相同元素的个数
    for(int i = 0; i < L.length; i++){
        if(L.data[i] != e){
            L.data[k] = L.data[i];
            k++;
        }
    }
    L.length = k;
    
    // 方法2
    // int k = 0; // 相同元素个数
    // for(int i = 0; i < L.length; i++){
    //     if(L.data[i] == e){
    //         k ++;
    //     }else {
    //         L.data[i - k] = L.data[i]; // 元素前移
    //     }
    // }
    // L.length = L.length - k;
}


int main(void){
    SqList L;
    ElemType del;

    L.data[0] = 1;
    L.data[1] = 2;
    L.data[2] = 2;
    L.data[3] = 4;
    L.data[4] = 5;
    L.length = 5;

    PrintfList(L);
    Delete_Elem_x(L, 2);
    PrintfList(L);

    return 0;
}

2.2.3-4,5

/**
 * 删除线性表中所有值为 s 到 t 的数据元素。
 */
#include <iostream>
#include <stdio.h>
using namespace std;

#define MaxSize 50
typedef int ElemType;

typedef struct{
    ElemType data[MaxSize];
    int length;
} SqList;

void PrintfList(SqList L){
    for(int i = 0; i < L.length; i++){
        printf("%4d", L.data[i]);
    }
    printf("\n");
}

bool Delete_between(SqList &L, ElemType s, ElemType t){
    
    // 我的做法
    if(!(s < t) && L.length == 0){
        return false;
    }
    int k = 0;
    for(int i = 0; i < L.length; i++){
        if(L.data[i] < s || L.data[i] > t){
            L.data[k] = L.data[i];
            k++;
        }
    }
    L.length = k;
    return true;
}

int main(void){
    SqList L;
    ElemType del;

    L.data[0] = 1;
    L.data[1] = 2;
    L.data[2] = 2;
    L.data[3] = 4;
    L.data[4] = 5;
    L.length = 5;

    PrintfList(L);
    Delete_between(L, 2, 4);
    PrintfList(L);

    return 0;
}

2.2.3-6

/**
 * 删除线性表中重复的数据元素。
 */
#include <iostream>
#include <stdio.h>
using namespace std;

#define MaxSize 50
typedef int ElemType;

typedef struct{
    ElemType data[MaxSize];
    int length;
} SqList;

void PrintfList(SqList L){
    for(int i = 0; i < L.length; i++){
        printf("%4d", L.data[i]);
    }
    printf("\n");
}

bool Delete_repetition(SqList &L){

    if(L.length == 0){
        return false;
    }

    int k = 0;
    for(int i = 0; i < L.length; i ++){
        if(L.data[i] != L.data[i + 1]){
            L.data[k] = L.data[i];
            k++;
            // 高级写法 L.data[k++] = L.data[i];
        }
    }

    L.length = k; // 虽然 K 起始于 0,表示下标,通常总长度要k+1,但最后执行了++
    return true;
}

int main(void){
    SqList L;
    ElemType del;

    L.data[0] = 1;
    L.data[1] = 2;
    L.data[2] = 2;
    L.data[3] = 2;
    L.data[4] = 5;
    L.data[5] = 5;
    L.length = 6;

    PrintfList(L);
    Delete_repetition(L);
    PrintfList(L);

    return 0;
}

2.2.3-7

/**
 * 两个有序顺序表合并成一个顺序表。
 */
#include <iostream>
#include <stdio.h>
using namespace std;

#define MaxSize 50
typedef int ElemType;

typedef struct{
    ElemType data[MaxSize];
    int length;
} SqList;

void PrintfList(SqList L){
    for(int i = 0; i < L.length; i++){
        printf("%4d", L.data[i]);
    }
    printf("\n");
}

// 标准答案,经典算法,应该记住。两个有序表连接成新的有序表
bool Connect_List2(SqList &L1, SqList &L2, SqList &LBig){
    if(L1.length + L2.length > LBig.length){
        return false;
    }

    int i = 0, j = 0, s = 0;
    while(i < L1.length && j < L2.length){
        if(L1.data[i] < L2.data[j]){
            LBig.data[s++] = L1.data[i++];
        }else{
            LBig.data[s++] = L2.data[j++];
        }
    }

    while(i < L1.length){
        LBig.data[s++] = L1.data[i++];
    }

    while(j < L2.length){
        LBig.data[s++] = L2.data[j++];
    }

    LBig.length = s;

    return true;
}

bool Connect_List(SqList &L1, SqList &L2, SqList &LBig){
    // 第一个答案,错误,没有考虑到“有序”,不能简单做连接工作
    // for(int i = 0; i < L1.length; i++){
    //     LBig.data[i] = L1.data[i];
    // }

    // for(int j = 0; j < L2.length; j++){
    //     LBig.data[L1.length + j] = L2.data[j];
    // }

    // 改良版,外层循环,再内层循环,将小的存入新表
    int t = 0; // L2表的次序码
    int s = 0; // 总表的次序码
    for(int i = 0; i < L1.length; i++){
        while(t < L2.length){
            if(L1.data[i] <= L2.data[t]){
                LBig.data[s++] = L1.data[i];
                break; // 取到 L1 中的值,必须要break L2的 while
            }
            else{
                LBig.data[s++] = L2.data[t];
                t++;
            }
        }
    }

    if(t < L2.length){ // 防止 L1 循环完,L2 直接 break, 使 L2 中的值丢失。
        for(int j = t; j < L2.length; j++){ // 取 break 之后的 t, L2.data[t] ~ L2.data[L2.length]
            LBig.data[s++] = L2.data[j];
        }
    }

    // 简化流行的写法,while直接做判断和循环,见标准答案
    // int j = t;
    // while(t < L2.length){
    //     LBig.data[s++] = L2.data[j++];
    // }

    LBig.length = s;

    return true;
}



int main(void){
    SqList L1;
    SqList L2;
    SqList LBig;
    ElemType del;

    L1.data[0] = 1;
    L1.data[1] = 3;
    L1.data[2] = 5;
    L1.length = 3;

    L2.data[0] = 2;
    L2.data[1] = 4;
    L2.data[2] = 6;
    L2.data[3] = 8;
    L2.length = 4;

    LBig.length = 7;

    PrintfList(L1);
    PrintfList(L2);
    PrintfList(LBig);

    Connect_List(L1, L2, LBig);

    PrintfList(L1);
    PrintfList(L2);
    PrintfList(LBig);

    return 0;
}

2.2.3-8

/**
 * 数组中两个线性表,编写函数实现两个线性表位置调换
 */
#include <iostream>
#include <stdio.h>

using namespace std;

#define MaxSize 50
typedef int ElemType;

typedef struct{
    ElemType data[MaxSize];
    int length;
} SqList;

void PrintfList(SqList L){
    for (int i = 0; i < L.length; i++){
        printf("%4d\n", L.data[i]);
    }
    printf("\n");
}

int main(void){
    SqList L;
    ElemType del;

    L.data[0] = 1;
    L.data[1] = 3;
    L.data[2] = 5;
    L.length = 3;
    
    PrintfList(L);

    return 0;
}

2.2.3-9

/**
 * 数组中两个线性表,编写函数实现两个线性表位置调换
 */
#include <iostream>
#include <stdio.h>

using namespace std;

#define MaxSize 50
typedef int ElemType;

typedef struct{
    ElemType data[MaxSize];
    int length;
} SqList;

void PrintfList(SqList L){
    for (int i = 0; i < L.length; i++){
        printf("%4d\n", L.data[i]);
    }
    printf("\n");
}

// 经典算法,折半查找
int Binary_Search(SqList L, ElemType x){

    int low = 0, high = L.length - 1, mid;

    while (low <= high){
        mid = (low + high) / 2;

        if (L.data[mid] == x){
            return mid;
        } else if(x < L.data[mid]){
            high = mid - 1;
        } else {
            low = mid + 1;
        }
    }
    return -1;
}

int SearchExchangeInsert(SqList &L, ElemType x){
    int low = 0, high = L.length, mid;

    while (low <= high){
        mid = (low + high) / 2;

        if (L.data[mid] == x){
            int temp = L.data[mid];
            L.data[mid] = L.data[mid + 1];
            L.data[mid + 1] = temp;
            return 1; // 替换操作

        } else if (x < L.data[mid]){
            high = mid - 1;
        } else {
            low = mid + 1;
        }
    }

    
    //全部折半完无匹配
    if (low > high){
        // low 即最后一次折半所在的地方
        for(int i = L.length; i > low - 1; i--){
            L.data[i] = L.data[i - 1];
        }
        printf("%d,%d,%d\n", mid, low, high);
        L.data[high + 1] = x;
        L.length++;

        return 2; // 插入操作
    }

    return -1;
}

int main(void){
    SqList L;
    ElemType del;

    L.data[0] = 11;
    L.data[1] = 22;
    L.data[2] = 33;
    L.data[3] = 35;
    L.data[4] = 55;
    L.data[5] = 66;
    L.data[6] = 77;
    L.length = 7;
    
    PrintfList(L);
    printf("%d\n", SearchExchangeInsert(L, 34)); 
    PrintfList(L);

    return 0;
}
]]>
数据结构数据结构
<![CDATA[以RSA加密传输密钥(JS-PHP)]]>https://ariser.cn/index.php/archives/31/

介绍

网页表单提交中,如果直接用明文传输,特别是用户密码,很容易被抓包获取到信息。
这里以AJAX向PHP后台提交数据为例,用RSA对表单数据进行加密传输。

RSA简介

  1. RSA公钥加密算法是1977年由Ron Rivest、Adi Shamirh和LenAdleman在(美国麻省理工学院)开发的。RSA取名来自开发他们三者的名字。
  2. RSA是目前最有影响力的公钥加密算法,它能够抵抗到目前为止已知的所有密码攻击,已被ISO推荐为公钥数据加密标准。目前该加密方式广泛用于网上银行、数字签名等场合。
  3. RSA算法基于一个十分简单的数论事实:将两个大素数相乘十分容易,但那时想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。

算法核心

RSA的算法涉及三个参数,n、e1、e2。
其中,n是两个大质数p、q的积,n的二进制表示时所占用的位数,就是所谓的密钥长度。
e1和e2是一对相关的值,e1可以任意取,但要求e1与(p-1)*(q-1)互质;再选择e2,要求(e2*e1)mod((p-1)*(q-1))=1。
(n,e1),(n,e2)就是密钥对。其中(n,e1)为公钥,(n,e2)为私钥。[1]  
RSA加解密的算法完全相同,设A为明文,B为密文,则:A=B^e2 mod n;B=A^e1 mod n;(公钥加密体制中,一般用公钥加密,私钥解密)
e1和e2可以互换使用,即:
A=B^e1 mod n;B=A^e2 mod n;

实现流程

  1. 客户端公钥加密
  2. 服务端私钥解密
  • 生成公钥和私钥

~~这里用到支付宝提供一键生成工具,便于开发者生成一对RSA密钥,传送门
下载安装后,按照里面的教程生成一组公钥和私钥,目录结构如下:~~

上述用支付宝RSA生成工具的方法容易造成PHP识别不了密钥,这里用Openssl自动生成,参考:PHP RSA加密解密

  • Unix/Linux自带openssl,直接控制台运行:

    1. 私钥 :openssl genrsa -out rsa_private_key.pem 1024(去掉1024默认生成的是2048位)
    2. 公钥:openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
  • Windows需要另外下载安装openssl,参考:使用OpenSSL生成RSA证书

生成以下文件:

rsa_private_key.pem //私钥文件
rsa_public_key.pem  //公钥文件
rsa_private_key_pkcs8.pem //暂时用不上
  • 生成JS的RSA类库

    运行build_js.php生成RAS.js
    (要将build_js.php 和 生成的rsa_public_key.pem 放到同一目录下再命令行php build_js.php

  • JS引用RSA类库

<script type="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script src="RSA.js"></script>
<script>
    function f() {
        var username = $("#inputText1").val();
        var pswd = $("#inputText2").val();
        $.ajax({
            url:'json_test.php',
            data:{"username":rsa_encode(username), "pswd":rsa_encode(pswd)},
            type:'post',
            success: function(data){
                alert(data);
                window.location.reload()

            },
            error: function(XMLHttpRequest, textStatus, errorThrown) {
                alert(XMLHttpRequest.status);
                alert(XMLHttpRequest.readyState);
                alert(textStatus);
            },
        });
    }
</script>
  • PHP解密数据(用法可见function.php

<?php
/**
 * RSA私钥解密,需在php.ini开启php_openssl.dll扩展
 * @param String : after_encode_data 前端传来,经 RSA 加密后的数据
 * @return 返回解密后的数据
 */
function rsa_decode($after_encode_data)
{
    // 读取私钥文件
    $private_key = file_get_contents('rsa_private_key.pem');
    openssl_private_decrypt(
        base64_decode($after_encode_data),
        $decode_result,
        $private_key
    );
    return $decode_result;
}
?>

效果展示

调试窗口

]]>
PHP,Web开发,Web安全,JavaScript
<![CDATA[改良的MD5密码储存方案]]>https://ariser.cn/index.php/archives/18/

曾逛过一个服务器后台,发现就算看到数据库信息,也无法破解Admin用户的密码,因为MD5是不可逆的,稍微复杂一点就基本不可能破解。开始一直采用RSA存储密码还是存在破解的风险(进入到后台以及源码中的私钥)。

  1. 这里稍作改进,加密算法为:1. MD5[MD5[密码]+username]。2. 截取从第10截取16位长度
  2. PHP脚本实现如下:
$username = '////';
$password = '////';
$pswdCode = md5($password);
$mixCode = md5($pswdCode.$username);
$realCode = substr($mixCode, 9, 16);
echo $realCode;
]]>
Web开发
<![CDATA[微信图片地址转换(简单图床程序)]]>https://ariser.cn/index.php/archives/19/在:微信文章归档系统开发过程中,直接<img src="///">引用微信公众号里面的图片链接(打开文章后,图片上右键->新标签页打开),会出现以下情况:

解决方案:图片存到本地使用本地URL(图片格式webp很强大,也是微信用的,动图静图二合一的格式)

  1. 用户添加或修改首页图片操作的时候:
  2. 传到后台的图片URL先下载图片写入到本地(外部可访问)
  3. 根据时间戳生成图片名称
  4. 返回本地图片的URL

相当于一个简易的图床功能,源码如下:

/**
    * 简单图床程序,解决微信图片防止盗链问题,要设置www-data对img文件夹的写入权限
    * @desc 输入微信图片URL,存入本地,返回本地URL,涉及文件写入操作,不要放在common
    * @param $url
    * @return string
    */
private function simplePictureBed($url){
    $date = date('Ymd-His', $_SERVER['REQUEST_TIME']);
    $file_name = $date.'.webp';//拼接图片名称
    
    $img = file_get_contents($url);
    file_put_contents('/webdata/article/public/img/'.$file_name, $img);
    return 'http://'.$_SERVER['HTTP_HOST'].'/article/public/img/'.$file_name;
}

public function update($id, $newData) {
    $newData['img'] = $this->simplePictureBed($newData['img']);//转换图片地址
    
    $model = new ModelArticle();
    return $model->update($id, $newData);
}
]]>
PHP
<![CDATA[macOS安装Golang开发环境]]>https://ariser.cn/index.php/archives/119/

安装前需要Homebrew环境

安装Homebrew:

命令行输入:
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
另外建议切换国内源:传送门

安装并配置golang:

查看可用的go版本

brew search go
看到go@x.x即为可用版本。

安装go

brew install go@1.9

安装成功后,配置环境变量

vim ~/.bashrcvim ~/.zshrc

根据你实际情况而定。如zsh就是编辑~/.zshrc文件, bash就是~/.bash_profile文件,官方教程默认是vim ~/.bashrc

    #GOROOT
    export GOROOT=/usr/local/opt/go\@1.9

    #GOPATH
    export GOPATH=$HOME/Documents/code/gopath

    #GOPATH root bin
    export PATH=$PATH:$GOROOT/bin

source ~/.zshrc,使改动生效。

  • 其中,#GOPATH对应的文件夹,是你自己的go项目目录所在文件夹。

验证是否安装成功

go env
出现内容,即表示go环境安装配置成功

配置集成编译器Goland

JetBrian官网下载安装并激活Goland,需要配置以下两个地方:
直接传送门

]]>
Web开发,macOS,默认分类,Golanggolang
<![CDATA[下拉菜单点击刷新页面]]>https://ariser.cn/index.php/archives/23/直接看代码:

<select id="select1">
<!-- /// --!>
</select>

<script>
//下拉菜单点击,加载数据
$("#select1").change(function(){//左侧写入ID
    let typeNum = $(this).children('option:selected').val();
    refresh(typeNum);//页面加载函数
});
</script>
]]>
Web开发,JavaScript
<![CDATA[bootstrap.paginator.min.js分页插件的使用]]>https://ariser.cn/index.php/archives/20/bootstrap-paginator.min.js是基于Bootstrap v2/v3的分页插件,但诸如AdminLTE3等采用最新Bootstrap4的前端框架,使用后根本不会出现样式。这里到处爬贴找到了解决方法:解决bootstrap4 使用 bootstrap-paginator不显示样式的问题

项目中的 /public/static/js/bootstrap-paginator.min.js是改进后的版本,适配Bootstrap v4;

插件基于Ajax的使用方法:Bootstrap-paginator + PageHelper 前后台分页对接

当然需要对接口进行设计

  • 后端接口所需参数
    /**
     * 获取分页列表特定数据(title+url)
     * @desc 根据类型筛选列表数据,支持分页,用于用户
     * @return array    items   列表数据
     * @return int      total   总数量
     * @return int      page    当前第几页
     * @return int      perpage 每页数量
     */

    public function getList(){
        $rs = array();

        $domain = new DomainArticle();
        $list = $domain->getList($this->type, $this->page, $this->perpage);

        $rs['items'] = $list['items'];
        $rs['total'] = $list['total'];
        $rs['page'] = $this->page;
        $rs['perpage'] = $this->perpage;

        return $rs;
    }
  • 数据库分页查询算法
public function getList($type, $page, $perpage) {
    $sql = 'SELECT a.*, t.name'.'
        FROM wechat_article  a ,article_type t
        WHERE a.type = :types AND a.type = t.type
        ORDER BY a.orders DESC
        LIMIT :limit1, :limit2';
    
    $params = array(':types' => $type, ':limit1' => ($page - 1) * $perpage, ':limit2' => $perpage);
    
    return $this->getORM()->queryAll($sql, $params);
//  没有定义关联字段的方法,所以直接原生SQL。因为栏目是动态未知的,所以不能直接在JS中case更改样式
//        return $this->getORM()
//            ->select('*, article_type.name')
//            ->where('type', $type)
//            ->order('orders DESC')
//            ->limit(($page - 1) * $perpage, $perpage)
//            ->fetchAll();
}
  • Ajax调用:
let perpage = 8; //每页数目
let json_data = {
        "type": typeNum,
        "page": 1,
        "perpage": perpage,
    };
json_data[token_key] = getCSRF();

function refresh(typeNum) {
    renderSelect('#select1', typeNum);
    renderSelect('#select3', typeNum);//添加模态框下拉菜单
    $('#total').empty();
    $('#table1').empty();
    let perpage = 8;
    $.ajax({ 
        url: "/article/public/?s=Article/MainList",
        type: "POST",
        async: false,//关闭异步加载,因为后台的csrf_token有时效性
        data: json_data,

        success: function(res, status, xhr){
            let data = res.data;
            let items = data.items;
            Article_items = items;
            console.log(res);
            if (!res.ret || res.ret != 200) {
                console.log(res.msg);
                $('#table1').append('<tr><td>出错了┗|`O′|┛,请联系管理员!</tr></td>');
                return;
            }
            $('#total').append(data.total);
            let str = '';
            if(data.total == 0){
                $('#table1').append('<tr><td>本栏目还没有发表文章!该更新了(๑•̀ㅂ•́)و✧</td></tr>');
                return;
            }

            for(let i = 0; i < items.length; i++){
                str = '<tr><td>'+items[i].name+'</td><td>'+items[i].orders+'</td><td><a href="'+items[i].url+'" target="_blank">'+items[i].title+'</a></td><td>'+items[i].addtime+'</td><td class="text-center"><a id="edit" title="编辑"><i class="fa fa-edit text-primary"></i></a>&nbsp;&nbsp;&nbsp;<a id="delete" title="删除"><i class="fa fa-trash text-danger"></i></a></td></tr>';
                $('#table1').append(str);
            }
            

            var options = {
                bootstrapMajorVersion:3, //bootstrap的版本要求
                currentPage:data.page,//当前页数
                totalPages:Math.ceil(data.total/data.perpage),//总页数,向上取整
                numberOfPages:data.perpage,//每页记录数
                
                itemTexts: function(type, page, current){//设置分页样式
                    switch (type) {
                        case "first":
                            return "首页";
                        case "prev":
                            return "上一页";
                        case "next":
                            return "下一页";
                        case "last":
                            return "末页";
                        case "page":
                            return page;
                    }
                },
                onPageClicked: function(event, originalEvent, type, page){
                    $('#table1').empty();
                    let json_data = {
                        "type": typeNum,
                        "page": 1,
                        "perpage": perpage,
                    };
                    json_data[token_key] = getCSRF();
                    $.ajax({
                        url:"/article/public/?s=Article/mainList",
                        type:"POST",
                        async: false,
                        data: json_data,
                        success: function(res, status, xhr){
                            let data = res.data;
                            let items = data.items;
                            Article_items = items;
                            console.log(res);
                            if (!res.ret || res.ret != 200) {
                                console.log(res.msg);
                                $('#table1').append('<tr><td>出错了┗|`O′|┛,请联系管理员!</tr></td>');
                                return;
                            }
                            
                            let str = '';  
                            if(data.total == 0){
                                $('#table1').append('<tr><td>本栏目还没有发表文章!该更新了(๑•̀ㅂ•́)و✧</td></tr>');
                                return;
                            }
                            for(let i = 0; i < items.length; i++){
                                str = '<tr><td>'+items[i].name+'</td><td>'+items[i].orders+'</td><td><a href="'+items[i].url+'" target="_blank">'+items[i].title+'</a></td><td>'+items[i].addtime+'</td><td class="text-center"><a id="edit" title="编辑"><i class="fa fa-edit text-primary"></i></a>&nbsp;&nbsp;&nbsp;<a id="delete" title="删除"><i class="fa fa-trash text-danger"></i></a></td></tr>';
                                $('#table1').append(str);
                            }
                        }
                    })
                }
            };
            //初始化分页框
            element.bootstrapPaginator(options);
        },
        error: function(error){
            console.log(error);
            alert("出错了┗|`O′|┛,请联系管理员!");
        }
    });
    renderSelect('#select2', typeNum);//编辑模态框的下拉菜单
}
]]>
PHP,Web开发,JavaScript
<![CDATA[隐藏网站后台(设置链接密码)]]>https://ariser.cn/index.php/archives/22/看代码,在需要隐藏的页面首部加上:

$path_pass = 'pswd';    //载入页面的密码 http://host/Login.php?p=pswd
$path = isset($_GET['p'])?addslashes(trim($_GET['p'])):'';
if($path != $path_pass || !$path){
    header('Location: index.php');
    exit();
}

访问地址为:http://host/Login.php?p=pswd,不加链接密码会跳转到其他页面。

此方法也可以用于隐藏WordPress等博客后台的地址。

]]>
PHP,Web安全
<![CDATA[后台限制IP登陆错误次数]]>https://ariser.cn/index.php/archives/21/借助session实现,作为一个接口发布:

public function Index(){
    $rs = array();
    
    $ip = getIP::Index();

    if(!isset($_SESSION[$ip])){
        $_SESSION[$ip] = 5;
    }

    $rs['code'] = -1;

    if(isset($_SESSION[$ip]) && $_SESSION[$ip] > 0)
    {
        $domain = new DomainAdmin();
        $flag = $domain->ifUser($this->username, $this->pswd);

        if($flag == true){
            $_SESSION['adminadmin'] = true;
            $rs['href'] = "admin.php";
            $rs['code'] = 1;
            //登陆成功
        }else
            {
                $_SESSION['adminadmin'] = false;
                $_SESSION[$ip] --;
                $rs['code'] = 0;
                $rs['count'] = $_SESSION[$ip];
                //账号或密码错误,返回code = 0 和 可用次数count
            }
    }else
        {
            $rs['code'] = -1;
            $_SESSION['adminadmin'] = false;
            $rs['count'] = $_SESSION[$ip];
            //IP登陆次数用完,锁定,一直返回code = -1
        }
    return $rs;
}

页面头部检测:

//Login.php 登陆页面头部
if(isset($_SESSION[$ip]) && $_SESSION[$ip] == 0){
    echo '<script>window.location="404.html"</script>';
}

//admin.php 后台页面头部
if(!isset($_SESSION["adminadmin"]) || !$_SESSION["adminadmin"] === true || $_SESSION[$ip] < 0){
    echo '<script>window.location="Login.php"</script>';
}

登录界面JS:

 function f() {
        let username = $("#inputText1").val();
        let pswd = $("#inputText2").val();
        let csrf_token = $("#csrf_token").val();
        $.ajax({
            type:'POST',
            url:'/article/public/?s=Login/Index',
            data:{
                "username": rsa_encode(username),
                "pswd": rsa_encode(pswd),
                "csrf_token": csrf_token,
            },
            
            success: function(res, status, xhr){
                let data = res.data;
                console.log(res);
                if (!res.ret || res.ret != 200) {
                    console.log(res.msg);
                    alert('通信错误,请联系管理员!');
                    return;
                }
                
                if(data.code == 1)
                {
                    window.location = data.href;
                }else if(data.code == 0){
                    alert('账号或密码错误!剩余可用次数为:'+data.count);
                    window.location.reload();
                }else if(data.code == -1){
                    window.location="404.html";
                }
            },
            error: function(XMLHttpRequest, textStatus, errorThrown) {
                console.log(XMLHttpRequest.status);
                console.log(XMLHttpRequest.readyState);
                console.log(textStatus);
                console.log(errorThrown);
                alert('参数出错,请刷新后重试!');
            },
        });
    }
]]>
PHP,Web安全
<![CDATA[macOS Homebrew 更换中国源]]>https://ariser.cn/index.php/archives/118/步骤:

  • 找到brew.git和homebrew-core.git的本地repo
  • 切换两个repo的remote url

切换USTC源:https://lug.ustc.edu.cn/wiki/mirrors/help/brew.git

# 更换brew.git的源
cd "$(brew --repo)"
git remote set-url origin https://mirrors.ustc.edu.cn/brew.git
# 更换homebrew-core.git的源
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-core.git

brew update

更换Homebrew Bottle源,在shell配置文件里加上(或更改)一个变量。
如zsh就是编辑~/.zshrc文件, bash就是~/.bash_profile文件:

export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles
]]>
Hackintosh,macOSmacOS,开发环境
<![CDATA[记一次Redis在投票活动中的优化实践]]>https://ariser.cn/index.php/archives/422/

环境

macOS 10.14 + Redis + php7

为了验证Redis扩展是否安装好,可以运行下面的脚本

<?php
$redisObject=new Redis();
if(!$redisObject->connect('127.0.0.1',6379)){
    die("Can't connect to Redis Server");
}else{
    echo 'Redis is ready!';
}

需求和业务流程

投票活动,大量用户瞬间访问投票通道,投完之后要马上看到实时投票情况。

对于高并发环境下,直接进行MySQL写入或读取,会极大消耗服务器资源。

利用Redis缓存用户的操作(热数据),周期性保存到MySQL中(冷数据),然后把冷数据从Redis删除,周而复始

思路

用户投票直接写入Redis:vid:{uid, ip, name}
脚本检测CPU负载,当负载小于阈值的时候,将热数据取出来写入MySQL

投票

投票页面 index.html

三个按钮,使用Ajax调用投票接口

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
  <title>Document</title>
</head>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<body>
  <p><span id="uid1">0</span> <input type="button" value="用户1" onclick="vote(1);" /></p>
  <p><span id="uid2">0</span> <input type="button" value="用户2" onclick="vote(2);" /></p>
  <p><span id="uid3">0</span> <input type="button" value="用户3" onclick="vote(3);" /></p>
</body>
<script>
  getVotes(1);
  getVotes(2);
  getVotes(3);
  // 获取票数
  function getVotes(i){
    $.get('votes.php?uid='+i,function(rs){
      var span = '#uid'+i;
      $(span).html(rs);
    });
  }

  // 投票
  function vote(i){
    $.get('vote.php?uid='+i,function(rs){
      var span = '#uid'+i;
      $(span).html(rs);
    });
  }
</script>
</html>

投票接口 vote.php

逻辑:

  • 连接Redis服务器
  • 保存投票人uid,用uid作为键值存票数,返回给前端用
  • 使用 voteid_sum 作为键存储总票数。
  • 记录 uidiptime等数据 vid:{uid, ip, name}
<?php 
$redisObj = new Redis();
if(!$redisObj->connect('127.0.0.1', 6379)){
    die("Can't connect to Redis Server");
}

$uid = intval($_GET['uid']);
//$uid = mt_rand(1,3);//随机指定投票人员,方便进行压力测试
echo $redisObj->incr($uid);

// redis操作, 记录当前票的情况 vid:{uid, ip, name}
$voteid = $redisObj->incr('voteid_sum'); // incr是将 key 中储存的数字值增一
$redisObj->set('vote:'.$voteid.':uid', $uid);
$redisObj->set('vote:'.$voteid.':ip', $_SERVER['REMOTE_ADDR']);
$redisObj->set('vote:'.$voteid.':time', time());

// 下面是获票接口 votes.php
<?php 
$redisObj = new Redis();
if(!$redisObj->connect('127.0.0.1', 6379)){
    die("Can't connect to Redis Server");
}
$uid = intval($_GET['uid']);
echo $redisObj->get($uid);

冷热数据交换 swap.php

连接数据库和Redis

五秒钟循环一次,检测CPU负载,小于70的时候:从redis取出键值对,拼接SQL语句,执行写入操作。每次写完,记录 last值表示最后插入的位置,以便下次从这个之后插入

<?php
//连接数据库
$pdo = new PDO('mysql:host=127.0.0.1;dbname=redis_vote','root','root777');
$pdo->query('set names utf8');

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

// 循环
while(true){
    $cpu_used_status = get_used_status()['cpu_usage'];
    if($cpu_used_status < 70){
        $vid = $redis->get('voteid_sum');//自增长的主键
        $last = $redis->get('last');//最近一次插入mysql的投票主键
        //如果没有插入数据库,刚开始的肯定为true
        if(!$last){
            $last = 0;//设置为0
        }
        // 遍历到末尾,库都写完
        if($vid == $last){
            echo "Completed, waitting for data\n";//输出等待
        }else{
            // 写库
            $sql = 'insert into vote(vid, uid, ip, time) values';
            for($i = $vid; $i>$last; $i--){
                $k1 = 'vote:'.$i.':uid';
                  $k2 = 'vote:'.$i.':ip';
                $k3 = 'vote:'.$i.':time';
                $row = $redis->mget([$k1, $k2, $k3]); // 返回所有(一个或多个)给定 key 的值
                $sql .= "($i, $row[0], '$row[1]', $row[2]),"; // 拼接SQL
                $redis->delete($k1, $k2, $k3);
            }
            $sql = substr($sql, 0, -1);
            $pdo->exec($sql);
            $redis->set('last', $vid);//设置插入的主键位置
            echo "Data writing\n";
        }
    }
    sleep(5);// 时延五秒
}

function get_used_status(){
    $fp = popen('top -b -n 2 | grep -E "^(Cpu|Mem|Tasks)"',"r");//获取某一时刻系统cpu和内存使用情况
    $rs = "";
    while(!feof($fp)){
       $rs .= fread($fp,1024);
    }
    pclose($fp);
    $sys_info = explode("\n", $rs);
    $tast_info = explode(",", $sys_info[3]);//进程 数组
    $cpu_info = explode(",", $sys_info[4]);  //CPU占有量  数组
    $mem_info = explode(",", $sys_info[5]); //内存占有量 数组
    //正在运行的进程数
    $tast_running = trim(trim($tast_info[1], 'running'));
    //CPU占有量
    $cpu_usage = trim(trim($cpu_info[0], 'Cpu(s): '),'%us');  //百分比

    //内存占有量
    $mem_total = trim(trim($mem_info[0], 'Mem: '),'k total');
    $mem_used = trim($mem_info[1], 'k used');
    $mem_usage = round(100*intval($mem_used)/intval($mem_total),2);  //百分比
     
    /*硬盘使用率 begin*/
    $fp = popen('df -lh | grep -E "^(/)"',"r");
    $rs = fread($fp, 1024);
    pclose($fp);
    $rs = preg_replace("/\s{2,}/", ' ', $rs);  //把多个空格换成 “_”
    $hd = explode(" ", $rs);
    $hd_avail = trim($hd[3], 'G'); //磁盘可用空间大小 单位G
    $hd_usage = trim($hd[4], '%'); //挂载点 百分比
    //print_r($hd);
    /*硬盘使用率 end*/ 

    //检测时间
    $fp = popen("date +\"%Y-%m-%d %H:%M\"","r");
    $rs = fread($fp, 1024);
    pclose($fp);
    $detection_time = trim($rs);

    /*获取IP地址  begin*/
    /*
    $fp = popen('ifconfig eth0 | grep -E "(inet addr)"','r');
    $rs = fread($fp,1024);
    pclose($fp);
    $rs = preg_replace("/\s{2,}/",' ',trim($rs));  //把多个空格换成 “_”
    $rs = explode(" ",$rs);
    $ip = trim($rs[1],'addr:');
    */
    /*获取IP地址 end*/
    /*
    $file_name = "/tmp/data.txt"; // 绝对路径: homedata.dat
    $file_pointer = fopen($file_name, "a+"); // "w"是一种模式,详见后面
    fwrite($file_pointer,$ip); // 先把文件剪切为0字节大小, 然后写入
    fclose($file_pointer); // 结束
    */

    return  array(
        'cpu_usage' => $cpu_usage,
        'mem_usage' => $mem_usage,
        'hd_avail' => $hd_avail,
        'hd_usage' => $hd_usage,
        'tast_running' => $tast_running,
        'detection_time' => $detection_time
    );
}

数据表

表结构如下

+-------+------------------+------+-----+---------+----------------+
| Field | Type             | Null | Key | Default | Extra          |
+-------+------------------+------+-----+---------+----------------+
| vid   | int(11)          | NO   | PRI | NULL    | auto_increment |
| uid   | int(11)          | YES  |     | NULL    |                |
| ip    | char(20)         | YES  |     | NULL    |                |
| time  | int(10) unsigned | YES  |     | NULL    |                |
+-------+------------------+------+-----+---------+----------------+

脚本:

CREATE TABLE votes(
    vid int(11) PRIMARY KEY AUTO_INCREMENT,
    uid int(11),
    ip char(20),
    time int(10) unsigned 
);
]]>
Web开发Redis
<![CDATA[Mac配置环境Nginx]]>https://ariser.cn/index.php/archives/5/环境:MacOS 版本:High Sierra 10.13.5

目录:

  • 修改Apache端口号
  • 安装、配置nginx
  • nginx解析php

修改Apache端口号

Mac自带有apache服务,可以直接开启及配置

开启apache:  sudo apachectl start
重启apache:  sudo apachectl restart
关闭apache:  sudo apachectl stop

这里把80端口让给nginx,apache默认设置为8080

1.修改httpd.conf

sudo vim /etc/apache2/httpd.conf


vim下/80搜索字符,按n和N向上下选择其他项,将Listen后的端口号修改8080


这里用#注释做下备份,Esc wq保存

2.修改httpd-vhosts.conf

sudo vim /etc/apache2/extra/httpd-vhosts.conf

貌似有两个地方需要修改重启apache,localhost:8080,出现It works!代表修改成功:

sudo apachectl restart

没修改hosts的用127.0.0.1:8080

安装、配置nginx

使用brew安装nginx

brew install nginx

修改配置文件,端口设置为80
默认为8080端口

给几个权限:

sudo chown root:wheel /usr/local/Cellar/nginx/1.15.2/bin/nginx
sudo chmod u+s /usr/local/Cellar/nginx/1.15.2/bin/nginx
sudo chown -R root:wheel /usr/local/etc/nginx/

这里的版本号要根据自己的做修改,用 brew info nginx 来查看路径

重启下nginx

nginx -t
nginx -s reload
brew services restart nginx

浏览器localhost,出现以下界面则代表安装成功:

如果依然是It works,建议清理下浏览器缓存

nginx解析php

上一步完成,一般并不能直接访问php网站,localhost/index.php会直接下载或Nginx An error occurred,nginx这时并不能直接解析php。因为Mac系统的php-fpm通常开启不了。 通常sudo php-fpm会显示: ERROR: failed to open error_log (/usr/var/log/php-fpm.log): No such file or directory
以及:No pool defined. at least one pool section must be specified in config file

修改php-fpm配置:

sudo cp /private/etc/php-fpm.conf.default /private/etc/php-fpm.conf
/private/etc/php-fpm.conf

修改error_log路径:

error_log = /usr/local/var/log/php-fpm.log

这时即可sudo php-fpm开启。 访问localhost/index.php(这个文件自己写一个)依然会出现以下问题: 1.访问 index.php 报 403 Forbidden.

vim /usr/local/etc/nginx/nginx.conf

找到 server 的 location 配置,给 index 加一个 index.php

location / {
  root  html;
  index index.html index.htm index.php;
}

2.访问 index.php 报 File not found. 找到server 下被注释的 location ~.php$(删除代码前面的 ‘#')

location ~ \.php$ {
  root      html;
  fastcgi_pass  127.0.0.1:9000;
  fastcgi_index index.php;
  fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
  include    fastcgi_params;
}

且更改

fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;

为:

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

然后重启nginx:

sudo nginx -s reload //重载配置文件
sudo nginx -s stop //停止nginx服务
sudo nginx //开启nginx服务

参考的帖子: Mac下Nginx安装环境配置详解

Mac自带PHP启动php-fpm问题解决

php-fpm:No pool defined解决方法

]]>
Linux
<![CDATA[PHP-SPL标准库]]>https://ariser.cn/index.php/archives/343/相当于C里面STL

  1. $stack = new SplStack();
    $stack->push("data1\n");
    $stack->push("data2\n");
    echo $stack->pop();
    echo $stack->top();
    echo $stack->pop();
  2. 队列

    $queue = new SplQueue();
    
    $queue->enqueue("data1\n");
    $queue->enqueue("data2\n");
    
    echo $queue->dequeue();
    echo $queue->top();
    echo $queue->dequeue();
  3. $heap = new SplMinHeap();
    
    $heap->insert("data1");
    $heap->insert("data2");
    
    echo $heap->extract();
    echo $heap->extract();
  4. 固定长度的数组,其它位置为空

    $array = new SplFixedArray(10);
    $array[0] = 123;
    $array[9] = 456;
    var_dump($array);
]]>
PHP
<![CDATA[PHP魔术方法]]>https://ariser.cn/index.php/archives/341/

PHP魔术方法

  1. __get/__set:接管对象属性。定义私有属性和设置私有属性
  2. __call/__callStatic:控制对象的方法调用
  3. __toString:将PHP对象转换为字符串
  4. __invoke:将PHP对象当成函数执行时会调用这个方法

__get/__set

$obj = new Work\Objection();
echo $obj->title;

调用对象不存在的属性时会报错

应该在对象内设置__get/__set方法

class Objection{
    protected $array = array();
    
    function __set($key, $value){
        $this->array[$key] = $value;
    }
    
    function __get($key) {
        return $this->array[$key];
    }
}

这样一来,就不会报错。$obj->title = "hello";会调用set设置属性,echo $obj->title;会调用get获取属性

$obj = new Work\Objection();
$obj->title = "hello";
echo $obj->title;

__call/__callStatic

调用对象不存在的方法时会报错

$obj = new Work\Objection();
echo $obj->test("hello", 123);

这时设置在对象内设置__call方法

function __call($fun, $param) {
    var_dump($fun, $param);
    return "magic function";
}

调用到对象内不存在的方法时,会自动调用__call方法

static function __callStatic($name, $arguments) {
    var_dump($name, $arguments);
    return "magic function";
}
\Work\Objection::test("hello", 123);

__toString

$obj = new Work\Objection();
echo $obj;

对象本身不能当做字符串来输出,需要转换成字符串,应该有__toString方法自动回调

function __toString() {
    return __CLASS__;
}

__invoke

对象当成函数执行

$obj = new Work\Objection();
echo $obj("test1");
function __invoke($param) {
   var_dump($param);
   return "invoke";
}
]]>
PHP
<![CDATA[PHP PSR-0规范和类自动载入]]>https://ariser.cn/index.php/archives/344/

PSR-0

PSR-0是一个规范:

  1. 命名空间与绝对路径一致
  2. 类名首字母大写
  3. 除入口文件外,其他 .php必须只有一个类(不能执行)。

开发一个PSR-0规范的基础框架

  1. 全部使用命名空间
  2. 所有PHP文件必须自动载入,不能有include/require
  3. 单一入口index.php

类自动载入

<?php
spl_autoload_register('autoload1');
spl_autoload_register('autoload2');

Test1::test();
Test2::test();

function autoload1($class){
   require __DIR__.'/'.$class.'.php';
}

function autoload2($class){
   require __DIR__.'/xxx/'.$class.'.php';
}
]]>
PHP
<![CDATA[PHP链式操作的实现]]>https://ariser.cn/index.php/archives/340/

PHP链式操作的实现

eg:$db->where()->limit()->order(); 一行代码实现很多操作

传统方法:

<?php
namespace Work;

class Database {
    function where($where){
    }
    function order($order){
    }
    function limit($limit){
    }
}

调用:

$db = new Work\Database();
$db->where("id=1");
$db->where("name=2");
$db->order("id desc");
$db->limit(10);

链式实现:每个方法里面return $this;

<?php
namespace Work;

class Database {
    function where($where){
          return $this;
    }
    function order($order){
          return $this;
    }
    function limit($limit){
          return $this;
    }
}

调用:

$db = new Work\Database();
$db->where("id=1")->where("name=2")->order("id desc")->limit(10);
]]>
PHP
<![CDATA[PHP学习随笔]]>https://ariser.cn/index.php/archives/6/20180624
变量名称不能以数字开头
没有单独创建变量的命令,会在首次赋值的时候被创建
作用域: local(局部) global(全局) static(静态)

函数内部,global修饰函数内变量,可以访问函数内全局变量 global $x;

超级全局变量:

$x = 75;
$y = 25;
function addition()
{
    $GLOBALS['z'] = $GLOBALS['x'] + $GLOBALS['y'];
}
addition();
echo $z;//函数外调用z变量,输出结果为100

static:

function myTest(){
    $x=0;
    echo $x;
    $x++;
}

myTest();
myTest();
myTest(); //第二行,不加static,每次函数执行后会删除所有变量,加了static后这个局部变量不会被删除,保存的都是最后一次执行的值
//默认输出结果为1,static修饰$x后输出为3

单双引号都可以修饰字符串 var_dump()返回数据类型和值,显示为int(-123) 数组:

$cars=array(BMW,123,2131);
var_dump()的结果:
array(3) { [0]=> string(3) BMW [1]=> string(5) Volvo [2]=> string(4) saab }
$arrlength=count($cars); //求数组长度

关联数组:

$age=array(Peter=>35,Ben=>37,Joe=>43);等价于:
$age['Peter']=35;
$age['Ben']=37;
$age['Joe']=43;

遍历关联数组:

foreach($age as $x=>$x_value){ //$x是变量,$_x_value是变量值
    echo Key=.$x.,Value=.$x_value;
}

数组排序:

$cars=array(BMW,123,2131);
sort($cars); 按照字母/数字升序排列
rsort($cars); ------------降序排列

$age=array(Peter=>35,Ben=>37,Joe=>43);
asort($age); //按照数组的值升序
ksort($age); //按照数组的键升序
//对应的降序则是arsort和krsort

0625 字符串操作:

strlen(Hello world!);//求长度

并置运算符:

echo $text1. .$text2; //连接字符串

strpos() 函数用于在字符串内查找一个字符或一段指定的文本。 如果在字符串中找到匹配,该函数会返回第一个匹配的字符位置。如果未找到匹配,则返回 FALSE

echo strpos(Hello world!,world); //结果为6

x===y 绝对等于,等于且类型相同 xy 不等于

两种表单信息获取:

<form method="post" action="<?php echo $_SERVER['PHP_SELF'];?>">
Name: <input type="text" name="fname">
<input type="submit">
</form>
 
<?php 
$name = $_REQUEST['fname']; //$name = $_POST['fname']; 
echo $name; 
?>

魔术常量:

echo '函数名为:' . __FUNCTION__ ; //返回类名
echo '这是第  ' . __LINE__ . '  行';//某个php文件里面的
echo '该文件位于  ' . __FILE__ . '  '; //完整路径,直到文件E:\PHP\TEST\index.php
echo '该文件位于  ' . __DIR__ . '  ';//该文件在E:\PHP\TEST

function test() {
        echo '函数名为:' . __FUNCTION__ ;  //返回函数名
}


class a{
    function test(){
        echo '函数名为:' . __METHOD__ ; //返回的是a::test
  }
}

namespace MyProject;
echo '命名空间为:', __NAMESPACE__, ''; // 输出 MyProject

0626 晚上时间用于整理博客和家人视频聊天了,睡觉前闲预习面向对象,明天搞定,后天命名空间。 0627

PHP面向对象:

类:

定义了一件事物的抽象特点。类的定义包含了数据的形式以及对数据的操作。比如动物Animal就是一个抽象类。

对象:

在现实世界里我们所面对的事情都是对象,如计算机、狗、自行车等。狗和羊就是类的实例,它们有行为、形态。

类的定义、创建对象、实例化:

class Animal{
    /*成员变量*/
    var $animal;
    public $animal2 = 'i am an animal !'; //类中变量用var或者public定义,var默认为公有属性

    /*成员函数*/
    public function display(){
        echo $this->animal2;    //$this在php中称作为伪变量,用于访问成员变量
    }

    public function get_display($arr)
    {
        $this->animal = $arr;
        echo i am a .$this->animal. !;
    }

}

/*对象实例化*/
$sheep = new Animal;
$Dog = new Animal;

/*调用成员方法*/
$sheep->display();
$Dog->get_display('Dog');

构造函数:

>具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。
class Animal{
    /*成员变量*/
    var $animal = 'Pig';

    /*构造函数*/
    function __construct($ani1){
        $this->animal = $ani1;
    }

    function Shout(){
        echo i am .$this->animal;
        echo <br></br>;
    }
}

//$pig = new Animal();
//$pig->Shout();

/*自动调用了__construct()进行了初始化*/
$dog = new Animal('Dog');
$sheep = new Animal('Sheep');

$dog->Shout();
$sheep->Shout();

析构函数:

对象结束其生命周期时(例如对象所在的函数已调用完毕),系统自动执行析构函数。

class Animal{
    /*成员变量*/
    var $animal = 'Pig';

    /*构造函数*/
    function __construct($ani1){
        $this->animal = $ani1;
        echo 在构造函数中初始化成功,动物为.$this->animal.<br></br>;
    }

    function Shout(){
        echo i am a .$this->animal.<br></br>;
    }
    /*析构函数*/
    function __destruct(){
        echo 析构函数被调用;
    }


}

$dog = new Animal('Dog');
$dog->Shout();

继承:

extends 来继承一个类,PHP 不支持多继承

方法重写:

父类继承的方法不能满足子类的需求,可以对其进行改写,这个过程叫方法的覆盖(override),也称为方法的重写。

class Animal{
    var $animal;

    function __construct($ani1){
        $this->animal = $ani1;
        echo I am .$this->animal.<br></br>;
    }

    function Shout(){
            echo I can shout !<br></br>;
    }

    function Action(){
        echo I can move !<br></br>;
    }
}

/*继承父类*/
Class Dog extends Animal {
    /*重写父类方法*/
    function Action(){
        echo I can run !<br></br>;
    }
}

$animal = new Animal('Animal'); //输出: I am Animal
$animal->Shout();//输出: I am Animal
$animal->Action();//输出: I can action.
echo <br></br>;

$dog = new Dog('Dog');//输出:I am Dog !,证明父类构造函数自动被继承(PHP5及以后版本,PHP4不会自动继承父类构造方法)
$dog->Shout();//输出:I can shout !,父类方法被继承
$dog->Action('run'); //输出: I can run !,父类方法被重写

访问控制:

通过在前面添加关键字 public(公有),protected(受保护)或 private(私有)来实现访问控制。

  1. public(公有):公有的类成员可以在任何地方被访问。
  2. protected(保护):受保护的类成员则可以被其自身以及其子类和父类访问。
  3. private(私有):私有的类成员则只能被其定义所在的类访问。
class MyClass{
        public $public = '公有、';
        protected $protected = '受保护、';
        private $private = '私有<br></br>';
        
        function printHello(){
            echo $this->public;
            echo $this->protected;
            echo $this->private;
        }
    }
    
    $obj = new MyClass();
    
    echo $obj->public;//正常执行,输出:公有
    echo $obj->protected;//产生致命错误
    echo $obj->private;//产生致命错误
    
    $obj->printHello();//正常执行输出:公有、受保护、私有

方法的访问控制:

类中的方法可以被定义为公有,私有或受保护。如果没有设置这些关键字,则该方法默认为公有。

首先定义一个类,里面三个类型的成员函数和一个合计的public函数。

class MyClass{
    public function __construct(){ }

    public function MyPublic(){ }

    protected function MyProtected(){}

    private function Myprivate(){ }

    function Foo(){//默认为公有方法
        $this->MyPublic();
        $this->MyProtected();
        $this->Myprivate();
    }
}

调用这四个成员方法:

$myclass = new MyClass;
$myclass->MyPublic();//正常执行
//$myclass->MyProtected();产生致命错误
//$myclass->MyPrivate();产生致命错误
$myclass->Foo();//public、protect、private都可以执行

对于子类继承:

class MyClass2 extends MyClass{
    //公有方法
    function Foo2(){
        $this->MyPublic();
        $this->MyProtected();
        //$this->Myprivate();致命错误,子类不能继承父类的私有方法
    }
}

继承然后重写:

class Bar{
    public function test(){
        $this->testPrivate();
        $this->testPublic();
    }

    public function testPublic(){
        echo Bar::testPublic<br></br>;
    }

    private function testPrivate(){
        echo Bar::testPrivate<br></br>;
    }
}

class Foo extends Bar
{
    public function testPublic() {
        echo Foo::testPublic<br></br>;
    }

    private function testPrivate() {
        echo Foo::testPrivate<br></br>;
    }
}

$myFoo = new foo();
$myFoo->test(); // 输出: 父类::testPrivate换行 子类::testPublic 换行 子类::testPublic
// 可见,重写父类函数,对private无效,对protected、public有效。

接口:

使用接口(interface),可以指定某个类必须实现哪些方法,但不需要定义这些方法的具体内容。

接口是通过 interface 定义,就像定义一个标准的类,但其中定义所有的方法都是空的。 接口中定义的所有方法都必须是公有,这是接口的特性。 要实现一个接口,使用 implements操作符。类中必须实现接口中定义的所有方法,否则会报一个致命错误。类可以实现多个接口,用逗号来分隔多个接口的名称。

抽象类

任何一个类,如果它里面至少有一个方法是被声明为抽象的,那么这个类就必须被声明为抽象的。
定义为抽象的类不能被实例化。 被定义为抽象的方法只是声明了其调用方式(参数),不能定义其具体的功能实现。 继承一个抽象类的时候,子类必须定义父类中的所有抽象方法;另外,这些方法的访问控制必须和父类中一样(或者更为宽松)。例如某个抽象方法被声明为受保护的,那么子类中实现的方法就应该声明为受保护的或者公有的,而不能定义为私有的。 首先定义两个接口,动物运动(走和跑)和活动(运动和活动)。

先放图:牧羊犬Bitzer和小羊Shaun—《Shaun the Sheep》很喜欢这部漫画

定义了一个抽象类Animal,包含Run和color两个方法(Dog和Sheep本身都具有的属性)。 定义了两个接口,分别是Write(Dog具有的功能)和Run_faster(Sheep具有的功能)。

abstract class Animal{
    //强制要求Dog类和Sheep类定义这些方法
    abstract protected function run();
    abstract protected function color($colors);
}

interface Write{//狗可以写字
    public function write();
}

interface Run_faster{//羊可以跑得更快
    public function run_faster();
}

定义Dog类继承动物类,实现Write接口。

class Dog extends Animal implements Write {
    private $Dog ;
    private $Colors;
    public function __construct(){
        $this->Dog = 'Dog';
    }

    protected function run(){
        echo $this->Dog. can run.<br></br>;
    }
    protected function color($color){
        $this->Colors=$color;
        echo $this->Dog. is .$this->Colors.. <br></br>;
    }
    public function write(){
        echo $this->Dog. can write. <br></br>;
    }
    public function Show(){
        $this->run();
        $this->color('yellow');
        $this->write();
    }
}

定义Sheep类继承动物类和实现Run_faster接口。

class Sheep extends Animal implements Run_faster {
    private $Sheep;
    private $Colors;
    public function __construct(){
        $this->Sheep = 'Sheep';
    }
    protected function run(){
        echo $this->Sheep. can run.<br></br>;
    }
    protected function color($color){
        $this->Colors=$color;
        echo $this->Sheep. is .$this->Colors.. <br></br>;
    }
    public function run_faster(){
        echo $this->Sheep. can run faster. <br></br>;
    }
    public function Show(){
        $this->run();
        $this->color('white');
        $this->run_faster();
    }
}

实例化Dog和Sheep

$dog = new Dog();
$sheep = new Sheep();

$dog->Show();
echo <br></br>;
$sheep->Show();

/**
 * 输出结果:
 *Dog can run.
 *Dog is yellow.
 *Dog can write.

 *sheep can run.
 *sheep is white.
 *sheep can run faster.
 */

一直以来对接口和抽象类两者概念没完全搞清楚。今天把两者放到一起对比和思考,有了更为深刻的理解。 这篇帖子:深入理解Java的接口和抽象类给了启迪。

接口:

大白话式解释 -

接口和抽象类理解

一句话总结,抽象类是对一种事物的抽象,而接口是对行为的抽象。

抽象的继承体现的“是不是” Dog和Sheep继承Animal这个抽象类,它们就属于这个动物这个种类了,可以run和具有color。

接口的实现体现的“有没有” Dog实现了Write这个接口,它就有了write这个功能。没有实现Run_faster接口,就没有run_faster的功能。 Sheep实现了Run_faster接口,它就可以run_faster。没有实现Write接口,也就没有write的功能。

常量:

常量: 类中始终保持不变的值。定义和使用常量的时候不需要使用 $ 符号。 常量的值必须是一个定值,不能是变量,类属性,数学运算的结果或函数调用。 自 PHP 5.3.0 起,可以用一个变量来动态调用类。但该变量的值不能为关键字(如 self,parent 或 static)。

class MyClass{
    const str = '常量值';
    function showConstant(){
        echo self::str.<br></br>;
    }
}

echo MyClass::str.<br></br>;

$classname = MyClass;
echo $classname::str . <br></br>; // 自 5.3.0 起

$class = new MyClass();
$class->showConstant();

echo $class::str . <br></br>; // 自 PHP 5.3.0 起
//输出四次常量值

卫冕冠军德国队输了,剩下一点面向对象明天了结然后开始选择性地学习相应知识了。Cookie、Session等。

Static关键字:

声明类属性或方法为 static(静态),就可以不实例化类而直接访问。 静态属性不能通过一个类已实例化的对象来访问(但静态方法可以)。 由于静态方法不需要通过对象即可调用,所以伪变量 $this 在静态方法中不可用。 静态属性不可以由对象通过 ->操作符来访问。 自 PHP 5.3.0 起,可以用一个变量来动态调用类。但该变量的值不能为关键字 self,parent 或 static。

class Foo{
    public static $my_static = 'foo';

    public function staticValue(){
        return self::$my_static;
    }
}
echo Foo::$my_static.<br></br>;//输出foo,无需实例化直接访问
$foo = new Foo();
echo $foo->staticValue().<br></br>;//输出foo

Final 关键字

PHP5新增。如果父类中的方法被声明为 final,则子类无法覆盖该方法。如果一个类被声明为 final,则不能被继承。
写的时候直接摆错,提示final方法不可被覆盖(重写)-

(子类)调用父类构造方法

PHP 不会在子类的构造方法中自动的调用父类的构造方法。要执行父类的构造方法,需要在子类的构造方法中调用 parent::__construct()

class BaseClass{
    function __construct(){
        echo 主类构造方法<br></br>;
    }
}

class ChildClass extends BaseClass{
    function __construct(){
        parent::__construct();
        echo 子类构造方法<br></br>;
    }
}
class OtherSubClass extends BaseClass {
    // 继承 主类 的构造方法
}

$obj = new BaseClass();//echo主类
$obj = new ChildClass();//echo主类换行echo子类
$obj = new OtherSubClass();//echo主类

$_GET和$_POST

$_GET 变量用于收集来自 method=get 的表单中的值。 $_POST 变量用于收集来自 method=post 的表单中的值。

前端表单:

<form action="chuli.php" method="post"><!--method="get"-->
姓名:<input type="text" name="name">
年龄:<input type="text" name="age">
    <input type="submit" value="提交">
</form>

chuli.php

$name = $_POST["name"];//获取到之后直接放入变量
$age = $_POST["age"];
echo "欢迎: ".$name."
";
echo "年龄为: ".$age."
";



点击提交按钮后: post方式的URL: 

http://127.0.0.1/phpstudy/file.php


get方式的URL: 

http://127.0.0.1/phpstudy/file.php?name=Aris&age=20


两者本质都是TCP链接: 从带有 POST 方法的表单发送的信息,对任何人都是不可见的,并且对发送信息的量也没有限制。 然而,由于变量不显示在 URL 中,所以无法把页面加入书签。

在 HTML 表单中使用 method=get 时,所有的变量名和值都会显示在 URL 中。 注释:所以在发送密码或其他敏感信息时,不应该使用这个方法! 然而,正因为变量显示在 URL 中,因此可以在收藏夹中收藏该页面。在某些情况下,这是很有用的。 
]]>
PHP,Web开发
<![CDATA[微信开发3-openid+session身份验证]]>https://ariser.cn/index.php/archives/7/

其实到了这里才算做真正的"开发",前面的环境部署属于运维部分,并没有涉及到微信API调用之类的。

目的是获取用户的openid,与数据库储存的用户信息进行对比,通过则启动session,后面调用session验证以授予访问页面的权限,否则跳转其它页面。(有点冗余,可以看下面的结构图)。

从用户操作的角度来说,会便利化用户的操作。用户登陆过了该公众号,我们获取到的openid对于每个公众号是唯一不变的。省去了用户下载APP、注册登陆等步骤。

openID:


在微信里面,每个用户关注公众号,就会产生对这个公众号唯一的28位openID(微信自动生成)。openID具有唯一性和不可变性,可以作为用户对某一公众号身份的凭证。

session:

业务流程:


下面分别讲获取openid和创建运用session。

配置网页授权获取用户基本信息


在公众号管理界面:网页服务->网页账号->修改
这里填入授权回调页面域名,就是自己的域名。比如www.xxx.com

1.获取code

用户点击自定义菜单的按钮后,会跳转到目标页面,这个是开发人员自己设定的url链接。
比如我们想跳转到www.xxx.com/index.php,此时我们并不是让用户直接跳转到这个页面,而是把链接进行处理。

https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxxxxxxxxxxxxxx
&redirect_uri=http://www.xxx.com/index.php
&response_type=code
&scope=snsapi_userinfo
&state=123#wechat_redirect

(这里为了方便查看换行写了,用的时候注意删去换行)
这里的xxxxxxxxxxxxxxxxxx为自己公众号的appid
&redirect_uri= 加上"回调链接",即要转到的地址;&response_type=加上"回调参数code",下一步会用到这个变量;
&scop=加上"snsapi_base 和 snsapi_userfo",不多解释,参考这个: 传送门
后面的&state写123就行

2.通过CODE向服务器请求Openid

这里用后台处理脚本实现,创建getOpenid.php,直接上代码:

<?php
function getOpenid(){
        $code = $_GET['code'];//获取code
        $appid = 'xxxxxxxxxxxxxxxxxx';
        $secret = 'ssssssssssssssssssssssssssssssss';
        $weixin = file_get_contents("https://api.weixin.qq.com/sns/oauth2/access_token?appid=".$appid."&secret=".$secret."&code="
                .$code."&grant_type=authorization_code");//通过code换取网页授权access_token,(利用code间接获取用户数据组)
        $jsondecode = json_decode($weixin); //对JSON格式的字符串进行编码
        $array = get_object_vars($jsondecode);//转换成数组(用户数据组)
        $openid = $array['openid'];//取出openid
        return $openid;
}
$openid = getOpenid();
?>
在要用到openid的地方,先引入文件 include ("getOpenid.php"),然后就直接可以用这个变量。

注意:这里建议最好取出来存入变量中。刚开始没有取出来,直接用到openid的时候就执行一次函数,这个时候有的时候根本获取不到。

最后找到原因:必须得先有"获取code"这一步的跳转,紧接getOpenid()才可以拿到openid。没有code回调参数的情况下执行这个函数是拿不到openid的。

另一种方法:$code = $_GET['code'];放到函数外部,存入到变量,就不需要每个页面都获取一次code,。函数内要先 global $code。

3.身份验证、创建session

紧接上一步,我们获取到了openid,就可以进行身份验证了。

例:openid1(未注册用户,不能访问站点) openid2(注册用户,可以访问站点)
于是我们用获取到的openid与数据库进行对比,返回相应的权限值。action.php:

<?php
include ("getOpenid.php");
$openid = getOpenid();

function ifUser(){
        global $openid;  //访问函数外全局变量,前面加上global
        /*
        *这里进行查数据库操作
        *$num为返回的行数
        *$num = $result->num_rows;
        */

        //如果已经存在该用户
        if ($num){
                session_start();//session启动
                $_SESSION["admin"] = true;//赋值真
        }else{//跳转其他界面
                echo '<script>window.location="Hello.html"</script>';
                die();
        }
}
?>
上面两个部分可以应该写到一起,要用的时候直接调用一个函数即可,这里还是以分开为准。

session参考的博客:传送门,以及传送门2

要访问的页面:www.xxx.com/index.php,我们就可以在index.php的开头写入判断部分。
通过则继续执行下方的代码,不通过则自动跳转其他界面。

<?php
include ("getOpenid.php");
$openid = getOpenid();
include ("action.php");
ifUser();//如果两个步骤放到一个PHP脚本,则只用引用一次,可以减少openid的出现次数
 
$admin = false; //防止全局变量造成安全隐患
session_start();//启动会话
if(!isset($_SESSION["admin"]) || !$_SESSION["admin"] === true){
    //session验证不通过就跳转到其它页面
    echo '<script>window.location="Hello2.php"</script>';
    die();
}
 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello Openid !</title>
</head>
<body>
    <?php 
    echo "Welcom !""<br>";
    echo "Openid is: ".&openid."<br>";
    ?>
</body>
</html>

思考:session可以作为储存的媒介,可以将诸如身份的信息存储到里面$_SESSION["admin"]='123';,要用的时候就进行调用 $x = $_SESSION["admin"]。

]]>
PHP,Web开发,微信开发
<![CDATA[微信开发2-微擎]]>https://ariser.cn/index.php/archives/8/创青春省赛搞完,期中考试考完,又可以闲下来搞这个了
尝试了下宝塔面板管理服务器,比较好用,于是准备结合微擎来往下开发

其实要一个自定义菜单转到外链的功能。
至于为什么用微擎:微信官方开发说明对新手不太友好,自己是卡在与服务器的绑定那停滞不前,官方给的web.py那些部署了还是连接失败,没脾气了。
微擎的话相当于将操作简化了,且功能十分多。 作为学习的话,后面还是会转向原生的开发。
环境: 腾讯云服务器(香港)centos7 + Xshell + 微信测试号

目录:

  • 宝塔面板安装及一些调试
  • 微擎&&微信公众号对接

宝塔面板安装及一些调试

宝塔面板,管理服务器更加方便,且有服务器安全监控,数据库和FPT等要稍微修改配置。

centos直接yum源安装:

yum install -y wget && wget -O install.sh http://download.bt.cn/install/install.sh && sh install.sh

(ps:一键安装的过程中最好是root身份,因为涉及到的一些目录可能需要权限)
安装之后的界面

安装之后停留在此,上面会给出宝塔的账号密码。 之后 http://XXXX:8888即可进入面板。(xxxx表示自己服务器地址或域名)

接下来是微擎的环境。微擎基于PHP,可用LNMP和LAMP,二选一。A和N分别指的是apache和nginx。
传送门:Centos 7.3搭建LNMP环境

当然,宝塔给出了一键部署:
给出了两种套件

两种方式的话,新手作为学习可以尝试手动SSH相应服务,修改配置。
学习到知识之后,可以一键部署。

这里给出几个设置的修改:

  1. FTP不能远程管理

    1. 服务器控制台和面板开放21端口
    2. 登陆宝塔面板管理系统,找到左侧的“软件管理”--“FTP软件”---点击“设置”
    3. 点击配置修改:查找“ForcePassiveIP”(位置188行左右)
    4. 删除前面的“#”将“192.168.0.1”修改为:服务器的IP地址,是服务器不是你客户端的IP地址!
    5. 重启FTP

2.MySql不能远程连接
服务器控制台和面板开放3361端口(安全问题,慎开,学会习惯用phpmyadmin)

微擎&&微信公众号对接

  • 安装微擎:

有了LNMP环境之后,我们把下载的微擎通过FTP上传到网站根目录,注意要解压出来

然后访问 http://你的域名/install.php 进行安装,之后设置参数

这里注意到要用微擎的管理员账号,如果忘记了,可以进行以下步骤: (类似微赞等平台通用,WordPress找时间测试下) 1,下载附件 password.php(第四页会给到源码和下载地址) 2,使用文本编辑器修改第一行 $auth = ‘你的访问密码, 如 :123456’; 3,上传至你的微擎根目录 4,访问:你的域名/password.php 操作并重置密码 5,删除 password.php (重要)

(温馨提示:请确定文件的USER权限!)
重置密码完成后需要尽快删除 password.php 避免资料泄露

  • 微擎与微信测试号对接:

微擎中打开:公众号->公众号设置
微擎设置界面

测试号/公众号里面: 先设置安全域名

两边进行对接和绑定,这里就对应微信官方文档给出的绑定服务器操作。

这里第一次终于直接绑定成功,之后项目原因换了服务器之后,绑定到另一个上面就又显示绑定失败,但微擎仍然可以对之进行管理。
可能这个是跟域名走的吧,原因是需要更新微擎版本。

这里给出项目测试号的二维码:

管理员密码就是WeLock。

### 附录:

password.php源码:

<?php //使用后记得删除文件
//因为使用造成的安全问题 本人概不负责
//转载请注明保留版权信息、网址和作者
//BY:http://www.ariser.cn
//定义你的访问密码后上传
$auth = '123';
 
define('IN_SYS', true);
require './framework/bootstrap.inc.php';
load()-?>web('template');
load()->web('common');
load()->model('user');
 
if($_W['ispost'] && $_GPC['auth'] == $auth && $auth != '') {
    $isok = true;
    $username = trim($_GPC['username']);
    $password = $_GPC['password'];
    if(!empty($username) && !empty($password)) {
        
        $member = user_single(array('username'=>$username));
        if(empty($member)) {
            message('输入的用户名不存在.');
        }
        $hash = user_hash($password, $member['salt']);
        $r = array();
        $r['password'] = $hash;
        pdo_update('users', $r, array('uid'=>$member['uid']));
        exit('<script>alert(密码修改成功, 请重新登陆, 并尽快删除本文件, 避免密码泄露隐患.);location.href = ./</script>');
    }
}
?>



    <meta charset="utf-8"></meta><meta content="IE=edge" http-equiv="X-UA-Compatible"></meta><meta content="width=device-width," initial-scale="1.0" name="viewport"></meta><link href="./resource/favicon.png" icon="" rel="shortcut"></link><title>密码找回工具 FOR 0.6 - 微擎 - 公众平台自助引擎 -  Powered by WE7.CC</title><link href="./web/resource/css/bootstrap.min.css" rel="stylesheet"></link><link href="./web/resource/css/font-awesome.min.css" rel="stylesheet"></link><link href="./web/resource/css/common.css" rel="stylesheet"></link><script src="./web/resource/js/require.js"></script><script src="./web/resource/js/app/config.js"></script><div class="main">
    <form action="" class="form-horizontal" enctype="multipart/form-data" form="" formcheck="" method="post" onsubmit="return">
        <div class="panel" panel-default="" style="margin:10px;">
            <div class="panel-heading">
                重置密码 <span class="text-muted">如果你的管理密码意外遗失, 请使用此工具重置密码, 重置成功后请尽快将此文件从服务器删除, 避免造成安全隐患</span>
            </div>
            <div class="panel-body">
                <?php if($isok) {??><div class="form-group">
                    <label class="col-xs-12" col-lg-2="" col-md-2="" col-sm-3="" control-label="">用户名:</label>
                    <div class="col-sm-9">
                        <input echo="" name="auth" type="hidden" value="<?php"></input> />
                        <input class="form-control" name="username" placeholder="请输入你要重置密码的用户名" type="text"></input></div>
                </div>
                <div class="form-group">
                    <label class="col-xs-12" col-lg-2="" col-md-2="" col-sm-3="" control-label="">新的登录密码:</label>
                    <div class="col-sm-9">
                        <input class="form-control" name="password" placeholder="" type="password"></input></div>
                </div>
                <?php } else {??><div class="form-group">
                    <label class="col-xs-12" col-lg-2="" col-md-2="" col-sm-3="" control-label="">请输入访问密码</label>
                    <div class="col-sm-9">
                        <input class="form-control" name="auth" placeholder="" type="password"></input></div>
                </div>
                <?php }??><div class="form-group">
                    <label class="col-xs-12" col-lg-2="" col-md-2="" col-sm-3="" control-label=""></label>
                    <div class="col-sm-9">
                        <button btn-block="" btn-primary="" class="btn" name="submit" type="submit" value="提交">提交</button>
                        <input name="token" type="hidden" value="{$_W['token']}"></input></div>
                </div>
            </div>
        </div>
    </form>
</div>
]]>
PHP,Web开发,微信开发,Linux
<![CDATA[微信开发1—测试平台部署]]>https://ariser.cn/index.php/archives/9/项目是微信开锁 借助微信的网页授权功能,获取用户openid等信息与数据库进行对比,来验证身份,从而开门

目录

  • 微信公众号的选择

  • 服务器的部署


微信公众号有三种类型:服务号、订阅号、企业号

这里以个人身份只能申请到最低级别的普通个人订阅号,什么都搞不了。 如果要申请更高级别的则要提供组织、企业等单位的资质证明,作为一个学生也很难实施(步骤繁琐、在一些流程需要老师出面)。
找老师简单交流后,知道了有微信公众号测试号,不需要验证什么的流程,直接拿来用,而且功能权限也很多,就很舒服。传送门:微信公众平台接口测试帐号申请

用自己的微信登陆即可

进去后是这样的
在页面中,你还可以看到微信的appID和appserect,在获得Token、修改创建自定义菜单或者其它需要验证权限的时候需要用到这两个密钥,要注意对这两个参数保密! 正式开发的时候,需要对URL和Token进行设置:URL即微信公众平台服务器请求你开发服务器的入口页面,注意不是你网站的域名!具体到网页。 Token相当于腾讯微信公众平台服务器与你服务器交互的密钥,在正式部署的时候,请务必设置的复杂一下,否则可能被黑客利用,伪装你的服务器向你的用户发送消息。 另外需要说明的是,微信公众平台有两个Token,名字一样,但是是两个概念。 一个是腾讯微信公众平台服务器和你的服务器交互的密钥,是通过在你的服务器上设置的; 另外一个是通过appID和appsecret 获取到的操作菜单、发送客服消息等所需的一个凭据,注意不要混淆! 其他操作参见:微信公众平台开发文档

服务器配置

安装/更新需要用到的软件

安装python2.7版本以上 安装web.py 安装libxml2, libxslt, lxml python

centos安装python2.7(且与python2.6共存教程)

更新yum和安装开发工具集:
yum -y update
yum groupinstall -y 'development tools'

安装python工具需要的软件包(不然安装setuptools和pip会出错,然后提示找缺少什么文件,所以提前装上):
yum install -y zlib-devel bzip2-devel openssl-devel xz-libs wget

用源码安装Python2.7:
wget http://www.python.org/ftp/python/2.7.13/Python-2.7.13.tar.xz //下载源码
xz -d Python-2.7.13.tar.xz   // 解压文件
tar -xvf Python-2.7.13.tar  // 进入解压后的文件夹
cd Python-2.7.13  //运行配置
./configure --prefix=/usr/local  // 编译和安装
make
make altinstall  //make altinstall不用影响原来的python版本

设置软连接:
ln -s /usr/local/bin/python2.7 /usr/bin/python //经过软连接以后我们再使用python命令的是时候就指向我们的2.7版本的python了

安装setuptools:
wget --no-check-certificate https://pypi.python.org/packages/source/s/setuptools/setuptools-1.4.2.tar.gz  //下载源码
tar -xvf setuptools-1.4.2.tar.gz   //解压文件
cd setuptools-1.4.2   // 进入解压后的文件夹
python2.7 setup.py install   // 安装


安装pip:
curl https://bootstrap.pypa.io/get-pip.py | python2.7
  • 安装web.py

方式一:到web.py官网下载源码,解压出来安装 python setup.py install方式二:easy install安装 上一步其实已经安装好setuptools,这里也可以用yum源来安装 yum `install python-setuptoolseasy_install web.pypip install web.py注意,以上命令适用于python2版本,python3版本的命令为: pip install web.py==0.40-dev1`

  • 安装libxml2, libxslt, lxml python

yum `install + 名称`

  • 编辑运行py程序:

创建main.py,vim编辑之后,python main.py运行 `

# -- coding: utf-8 --
  # filename: main.py
  import web
  
  urls = (
      '/wx', 'Handle',
  )
  
  class Handle(object):
      def GET(self):
          return hello, this is a test
  
  if name == '__main__':
      app = web.application(urls, globals())
      app.run()

如果出现“socket.error: No socket could be created“错误信息,可能为80端口号被占用,可能是没有权限,请自行查询解决办法。也执行命令:sudo python main.py 80 或者改为其它端口。
- 浏览器输入http://外网IP:80/wx检验是否成功:

![](http://111.230.220.47/wp-content/uploads/2018/05/2018051218342025.png) 
]]>PHP,Web开发,微信开发<![CDATA[实验三 Linux进程基本实验]]>https://ariser.cn/index.php/archives/10/

任务概要

  1. 实验题目
    在 Linux 环境下,用 C 语言编写一个程序,以树状结构(即体现父子关系)输出系统当前所有进程。
  2. 实验目的

    • 理解进程和程序、进程和线程的联系与区别;
    • 熟悉进程的重要数据结构、进程的状态、进程管理与控制及实现机制;
    • 练习获取进程执行的相关信息,理解进程的执行过程。
  3. 实验平台
    Vmware Tools 14、Xshell5、Ubuntu16
  4. 实验要求
    在 Linux 下,用C语言自己编写一个程序,要求能以树状结构(即能体现父子关系)打印出系统当前的所有进程;独立完成
  5. 设计思路和流程

    • Linux自带pstree
    • 编写pstree,分析思路
    • 编译运行

详细设计

  1. 系统自带pstree展示:

pstree命令以树状图显示进程间的关系(display a tree of processes)。ps命令可以显示当前正在运行的那些进程的信息,但是对于它们之间的关系却显示得不够清晰。在Linux系统中,系统调用fork()可以创建子进程,通过shell也可以创建子进程,Linux系统中进程之间的关系天生就是一棵树,树的根就是进程PID为1的init(systemd)进程。常用的参数如下:(因为pstree输出的信息可能比较多,所以最好与more/less配合使用。)
格式:pstree
以树状图显示进程,只显示进程的名字,且相同进程合并显示。
格式:pstree -p
以树状图显示进程,还显示进程PID。
格式:pstree <pid>
格式:pstree -p <pid>
以树状图显示进程PID为<pid>的进程以及子孙进程,如果有-p参数则同时显示每个进程的PID。
格式:pstree -a
以树状图显示进程,相同名称的进程不合并显示,并且会显示命令行参数,如果有-p参数则同时显示每个进程的PID。

  1. 自行设计pstree.c,思路
    在Linux内核中, /proc目录是一种文件系统,即proc文件系统,存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。/proc目录中包含许多以数字命名的子目录,这些数字表示系统当前正在运行进程的进程号,里面包含对应进程相关的多个信息文件。

本实验参考以上的pstree的结构,通过访问/proc 里面的含有进程号的文件夹,获取进程的信息,每个文件夹中都含有一个status,获取其中的pid,ppid及name,从而获取父进程的id,源程序中的数据结构,是为了方便完成,将获取的文件存入。

将每次读取一个status的信息存入到此结构中,将所有的进程文件夹全部存入到数组中,进行结构树的打印。打印过程中若发现其存在子进程,记录父进程,递归打印子进程,当无子进程时,返回父进程。

  1. 编译运行:
    touch pstree.c


写好的pstree放入其中,vim或直接图形界面复制

gcc -o pstree pstree.c


./pstree

打印出了进程树

proc -A 看到SSH进程号为2014

pstree 2014 -p 显示出的ssh进程的父子进程与打印出的进程树的结构显示的是一样的

  1. 实验体会:
    实验结果显示,进程树为层次递进的,父子进程的先后顺序表现得十分明显。所有进程的主进程都是systemd,每个进程都有其父进程统管。Linux的子进程的实现是用的fork(),且可以递归下去。打印的时候,如果下一层没有子进程,则返回父进程。

Linux 使用“进程控制块(PCB)”的数据结构 task_struct 来代表一个进程,该结构包含进程的所有信息。所有关联 PCB 通过链表被组织起来,就形成了父子关系。

  1. 实验源码:

    #include
    #include
    #include
    #include
    #include
    #include
    #include #include
    #include
    
    int s = 0;
    char default_path[1024] = "/proc/";
    typedef struct file_info {
    int pid;
    int ppid;
    char name[1024];
    int flag;
    int rec;
    }info;
    
    int my_getpid(char* str){
    int len = strlen(str);
    char num[10];
    int i,j,ret;
    
    if(strncmp(str,"Pid",3) == 0)
    {
    for(i = 0; i < len; i++) { if(str[i]>='0' && str[i]<='9') break; } for(j = 0;j < len - i;j++) { num[j] = str[i + j]; } num[j] = '\0'; ret = atoi(num); } else ret = 0; return ret; } int my_getppid(char *str) { int len = strlen(str); char num[10]; int i, j, ret; if(strncmp(str,"PPid",4) == 0) { for(i = 0;i < len;i++){ if(str[i] >= '0' && str[i] <= '9') break; } for(j = 0;j < len - i;j++){ num[j] = str[i + j]; } num[j] = '\0'; ret = atoi(num); } else ret = 0; return ret; } void print_pstree(info *file,int count,int ppid,int rec) { int i,j,k; for(i = 0;i < count;i++) { if(file[i].flag == 0 && file[i].ppid == ppid) { file[i].rec = rec + 1; file[i].flag = 1; for(k = 0;k < rec;k++) { printf(" >>>>");
    }
    printf("%s\n", file[i].name);
    print_pstree(file,count,file[i].pid,file[i].rec);
    }
    }
    
    }
    
    int main(){
    int i,j,k,total,s1,s2,count,t;
    char str[1024], dir[1024];
    struct dirent **namelist;
    strcpy(dir, default_path);
    total = scandir(dir,&namelist,0,alphasort);
    printf("path = %s, total = %d\n", dir, total);
    
    for(i = 0; i < total; i++) { strcpy(str, namelist[i]->d_name);
    if(str[0] >= '0' && str[0] <= '9') count++; } printf("process counts is %d\n",count); info file[1024]; i = 0; t = 0; while(i < total) { FILE *fp; char path[1024],name[1024]; int pid,ppid; strcpy(str,namelist[i]->d_name);
    strcpy(path,default_path);
    if(str[0] >= '0' && str[0] <= '9') { strcat(path, str); strcat(path,"/status"); fp = fopen(path,"r"); while(!feof(fp)) { fgets(str, 1024, fp); if((s1 = my_getpid(str)) != 0) { pid = s1; } if((s2 = my_getppid(str)) != 0) ppid = s2; if(strncmp(str, "Name", 4) == 0) { for(j = 4;j < strlen(str);j++) { if(str[j] >= 'a'&&str[j] <= 'z') break; } for(k = j;k < strlen(str);k++) { name[k - j] = str[k]; } name[k - j] = '\0'; } file[t].pid = pid; file[t].ppid = ppid; strcpy(file[t].name,name); } fclose(fp); t++; } i++; } int m; for( m = 0;m < count;m++) { file[m].flag = 0; file[m].rec = 0; } print_pstree(file,count,0,0); return 0; }
]]>
Linux
<![CDATA[Android开发初试 - 计算器]]>https://ariser.cn/index.php/archives/11/

目录

  • AndroidStudio
  • 简单计算器的实现
  • 遇到的问题及心得体会

开发工具-AndroidStudio

Android开发环境大致可分为以下几个部分:

  • jdk环境配置
    就不多做介绍,下载安装及配置环境变量
  • 编译器
    平常Java用的是IDEA,可以直接在这个里面添加SDK和ADT,eclipse也是一样

这里看了下知乎,貌似AndroidStudio专业一点,自带终端和logcat等,专为开发Android而生,于是就入坑了。

  • 安装Android SDK和ADT和AVD
    安卓的软件开发包就是Android SDK,可以自己手动安装然后到编译器里面配置也可以直接在AS里面一件配置好。

就直接传送门吧,过程比较顺利。对于ADT的作用,借用官方的一句话“为了使得Android应用的创建,运行和调试更加方便快捷,Android的开发团队为IDE定制了一个插件:Android Development Tools(ADT)。”,安装的过程也就是给编译器添加插件的过程。
AVD就是一个安卓虚拟机,方便调试,这里也可以插上自己的安卓机来调试

  • 配置AndroidStudio
    这里弄了两个东西。
  1. 装后发现自己的C盘少了好几G的空间,这肯定不能忍。这是由于AS默认SDK和ADT的位置是在C盘的,参考传送门。
  2. 代码配色:大爱Solarized Dark配色
  3. 另外一个好玩的插件:activate-power-mode.jar,感受一下:
  4. 还有几个插件,Sexy Editor(更酷炫的配色)、Translation(AS内置翻译插件)

整体结构

  • 布局
  • 事件监听
  • 细节调整
  • 编译运行、打包

详细介绍

项目创建好之后,需要对下面两个地方进行操作:

事件监听和计算功能:java/com.XXXXX.项目名/MainActivity
布局:res/layout/activity_main.xml

两部分代码如下:

activity_main.xml:

[ccen_xml]
&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        &gt;


        &lt;LinearLayout
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:orientation="horizontal"
                android:layout_weight="2"
                &gt;

                &lt;TextView
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:id="@+id/et_input"
                        android:textSize="40sp"
                        /&gt;
        &lt;/LinearLayout&gt;

        &lt;LinearLayout
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:orientation="horizontal"
                android:weightSum="4"

                &gt;

                &lt;Button
                        android:id="@+id/btn_clear"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1.5"
                        android:autoLink="none"
                        android:background="#D2D7D9"
                        android:text="C"
                        android:textAppearance="@style/TextAppearance.AppCompat.Display1" /&gt;

                &lt;Button
                        android:id="@+id/btn_del"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1.5"
                        android:background="#D2D7D9"
                        android:text="CE"
                        android:textAppearance="@style/TextAppearance.AppCompat.Display1" /&gt;

                &lt;Button
                        android:id="@+id/btn_divide"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:background="#D2D7D9"
                        android:text="/"
                        android:textAppearance="@style/TextAppearance.AppCompat.Display1" /&gt;
        &lt;/LinearLayout&gt;
        &lt;LinearLayout
                android:layout_width="fill_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:gravity="center"
                android:weightSum="4"
                &gt;

                &lt;Button
                        android:id="@+id/btn_7"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:background="#fafafa"
                        android:text="7"
                        android:textSize="30sp" /&gt;

                &lt;Button
                        android:id="@+id/btn_8"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:background="#fafafa"
                        android:text="8"
                        android:textSize="30sp" /&gt;

                &lt;Button
                        android:id="@+id/btn_9"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:background="#fafafa"
                        android:text="9"
                        android:textSize="30sp" /&gt;

                &lt;Button
                        android:id="@+id/btn_multiply"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:background="#D2D7D9"
                        android:text="×"
                        android:textAppearance="@style/TextAppearance.AppCompat.Display1" /&gt;
        &lt;/LinearLayout&gt;
        &lt;LinearLayout
                android:layout_width="fill_parent"
                android:layout_height="0dp"
                android:orientation="horizontal"
                android:layout_weight="1"
                android:weightSum="4"
                &gt;

                &lt;Button
                        android:id="@+id/btn_4"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:background="#fafafa"
                        android:text="4"
                        android:textSize="30sp" /&gt;

                &lt;Button
                        android:id="@+id/btn_5"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:background="#fafafa"
                        android:text="5"
                        android:textSize="30sp" /&gt;

                &lt;Button
                        android:id="@+id/btn_6"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:background="#fafafa"
                        android:text="6"
                        android:textSize="30sp" /&gt;

                &lt;Button
                        android:id="@+id/btn_minus"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:background="#D2D7D9"
                        android:text="-"
                        android:textAppearance="@style/TextAppearance.AppCompat.Display1" /&gt;
        &lt;/LinearLayout&gt;
        &lt;LinearLayout
                android:layout_width="fill_parent"
                android:layout_height="0dp"
                android:orientation="horizontal"
                android:layout_weight="1"
                android:weightSum="4"
                &gt;

                &lt;Button
                        android:id="@+id/btn_1"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:background="#fafafa"
                        android:text="1"
                        android:textSize="30sp" /&gt;

                &lt;Button
                        android:id="@+id/btn_2"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:background="#fafafa"
                        android:text="2"
                        android:textSize="30sp" /&gt;

                &lt;Button
                        android:id="@+id/btn_3"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:background="#fafafa"
                        android:text="3"
                        android:textSize="30sp" /&gt;

                &lt;Button
                        android:id="@+id/btn_plus"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="1"
                        android:background="#D2D7D9"
                        android:text="+"
                        android:textAppearance="@style/TextAppearance.AppCompat.Display1" /&gt;
        &lt;/LinearLayout&gt;

        &lt;LinearLayout
                android:layout_width="fill_parent"
                android:layout_height="0dp"
                android:orientation="horizontal"
                android:layout_weight="1"
                android:weightSum="4"
                &gt;


                &lt;Button
                        android:id="@+id/btn_0"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="2"
                        android:background="#fafafa"
                        android:text="0"
                        android:textSize="30sp" /&gt;


                &lt;Button
                        android:id="@+id/btn_equal"
                        android:layout_width="0dp"
                        android:layout_height="match_parent"
                        android:layout_weight="2"
                        android:background="#0DA7E7"
                        android:text="="
                        android:textAppearance="@style/TextAppearance.AppCompat.Display1" /&gt;

        &lt;/LinearLayout&gt;

&lt;/LinearLayout&gt;
[/ccen_xml]

MainActivity.java:

package com.example.ice.ariscalculator;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;



public class MainActivity extends AppCompatActivity implements View.OnClickListener{
        Button btn_0,btn_1,btn_2,btn_3,btn_4,btn_5,btn_6,btn_7,btn_8,btn_9,btn_del;
        Button btn_multiply,btn_divide,btn_plus,btn_minus;
        Button btn_clear,btn_equal;
        private TextView et_input;
        boolean clr_flag;    //判断et中是否清空
        @Override
        protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                //实例化对象
                setContentView(R.layout.activity_main);
                btn_0= (Button) findViewById(R.id.btn_0);
                btn_1= (Button) findViewById(R.id.btn_1);
                btn_2= (Button) findViewById(R.id.btn_2);
                btn_3= (Button) findViewById(R.id.btn_3);
                btn_4= (Button) findViewById(R.id.btn_4);
                btn_5= (Button) findViewById(R.id.btn_5);
                btn_6= (Button) findViewById(R.id.btn_6);
                btn_7= (Button) findViewById(R.id.btn_7);
                btn_8= (Button) findViewById(R.id.btn_8);
                btn_9= (Button) findViewById(R.id.btn_9);
                btn_plus= (Button) findViewById(R.id.btn_plus);
                btn_minus= (Button) findViewById(R.id.btn_minus);
                btn_multiply= (Button) findViewById(R.id.btn_multiply);
                btn_divide= (Button) findViewById(R.id.btn_divide);
                btn_clear= (Button) findViewById(R.id.btn_clear);
                btn_del= (Button) findViewById(R.id.btn_del);
                btn_equal= (Button) findViewById(R.id.btn_equal);
                et_input= (TextView) findViewById(R.id.et_input);

                //设置按钮的点击事件
                btn_0.setOnClickListener(this);
                btn_1.setOnClickListener(this);
                btn_2.setOnClickListener(this);
                btn_3.setOnClickListener(this);
                btn_4.setOnClickListener(this);
                btn_5.setOnClickListener(this);
                btn_6.setOnClickListener(this);
                btn_7.setOnClickListener(this);
                btn_8.setOnClickListener(this);
                btn_9.setOnClickListener(this);
                btn_plus.setOnClickListener(this);
                btn_minus.setOnClickListener(this);
                btn_multiply.setOnClickListener(this);
                btn_divide.setOnClickListener(this);
                btn_clear.setOnClickListener(this);
                btn_del.setOnClickListener(this);
                btn_equal.setOnClickListener(this);
        }

        @Override
        public void onClick(View v) {
                String str=et_input.getText().toString();
                switch (v.getId()){
                        case   R.id.btn_0:
                        case   R.id.btn_1:
                        case   R.id.btn_2:
                        case   R.id.btn_3:
                        case   R.id.btn_4:
                        case   R.id.btn_5:
                        case   R.id.btn_6:
                        case   R.id.btn_7:
                        case   R.id.btn_8:
                        case   R.id.btn_9:
                                if(clr_flag){
                                        clr_flag=false;
                                        str="";
                                        et_input.setText("");
                                }
                                et_input.setText(str+((Button)v).getText());
                                break;
                        case R.id.btn_plus:
                        case R.id.btn_minus:
                        case R.id.btn_multiply:
                        case R.id.btn_divide:
                                if(clr_flag){
                                        clr_flag=false;
                                        str="";
                                        et_input.setText("");
                                }
                                if(str.contains("+")||str.contains("-")||str.contains("×")||str.contains("/")) {
                                        str=str.substring(0,str.indexOf(" "));
                                }
                                et_input.setText(str+" "+((Button)v).getText()+" ");
                                break;
                        case R.id.btn_clear:
                                if(clr_flag)
                                        clr_flag=false;
                                str="";
                                et_input.setText("");
                                break;
                        case R.id.btn_del: //判断是否为空,然后在进行删除
                                if(clr_flag){
                                        clr_flag=false;
                                        str="";
                                        et_input.setText("");
                                }
                                else if(str!=null&amp;&amp;!str.equals("")){
                                        et_input.setText(str.substring(0,str.length()-1));
                                }
                                break;
                        case R.id.btn_equal: //单独运算最后结果
                                getResult();
                                break;
                }
        }

        private void getResult(){
                String exp=et_input.getText().toString();
                if(exp==null||exp.equals("")) return ;
                //因为没有运算符所以不用运算
                if(!exp.contains(" ")){
                        return ;
                }
                if(clr_flag){
                        clr_flag=false;
                        return;
                }
                clr_flag=true;
                //截取运算符前面的字符串
                String s1=exp.substring(0,exp.indexOf(" "));
                //截取的运算符
                String op=exp.substring(exp.indexOf(" ")+1,exp.indexOf(" ")+2);
                //截取运算符后面的字符串
                String s2=exp.substring(exp.indexOf(" ")+3);
                double cnt=0;
                if(!s1.equals("")&amp;&amp;!s2.equals("")){
                        double d1=Double.parseDouble(s1);
                        double d2=Double.parseDouble(s2);
                        if(op.equals("+")){
                                cnt=d1+d2;
                        }
                        if(op.equals("-")){
                                cnt=d1-d2;
                        }
                        if(op.equals("×")){
                                cnt=d1*d2;
                        }
                        if(op.equals("/")){
                                if(d2==0) cnt=0;
                                else cnt=d1/d2;
                        }
                        if(!s1.contains(".")&amp;&amp;!s2.contains(".")&amp;&amp;!op.equals("/")) {
                                int res = (int) cnt;
                                et_input.setText(res+"");
                        }else {
                                et_input.setText(cnt+"");}
                }
                //s1不为空但s2为空
                else if(!s1.equals("")&amp;&amp;s2.equals("")){
                        double d1=Double.parseDouble(s1);
                        if(op.equals("+")){
                                cnt=d1;
                        }
                        if(op.equals("-")){
                                cnt=d1;
                        }
                        if(op.equals("×")){
                                cnt=0;
                        }
                        if(op.equals("/")){
                                cnt=0;
                        } else {
                                et_input.setText(cnt+"");}
                }
                //s1是空但s2不是空
                else if(s1.equals("")&amp;&amp;!s2.equals("")){
                        double d2=Double.parseDouble(s2);
                        if(op.equals("+")){
                                cnt=d2;
                        }
                        if(op.equals("-")){
                                cnt=0-d2;
                        }
                        if(op.equals("×")){
                                cnt=0;
                        }
                        if(op.equals("/")){
                                cnt=0;
                        }
                        if(!s2.contains(".")) {
                                int res = (int) cnt;
                                et_input.setText(res+"");
                        }else {
                                et_input.setText(cnt+"");}
                }
                else {
                        et_input.setText("");
                }
        }
}

布局:

采用的是LinearLayout线性布局,外层竖线布局内层嵌套横向布局。
外层的竖线线性布局,可以让按钮紧贴,方便调整Testview和每个横行的比例关系

android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"

android:orientation="horizontal"

示意图:

颜色的选取是参考win10的计算器。
取色技巧:QQ截屏,鼠标指的位置就会出现RGB值

事件监听:

设置按钮点击的事件执行OnClickListener() -> 里面加入case语句为每个按钮设置功能 -> 编写计算部分的函数(有点乱且没考虑特殊情况,打算在实现多项运算的时候重写)

计算函数大致思路就是读取三组字符串,两个数字以及计算符,对数字字符转换为double类型,再做计算。

细节调整:(更改图标或名称)

  1. resdrawable 放置icon.png(此图片是你需要修改的图标);
  2. 修改AndroidManifest.xml : android:icon="@drawable/icon"
  3. 编译运行即可。

打包生成apk:
不多说,直接传送门

遇到的问题有以下几点:

1.编译好的APP放到手机或模拟器闪退

这个东西真的可以把你的脾气全部磨干净~~
找到的原因是:布局里面删掉了按钮,在事件里面只删除了case里面的功能,忘记删除实例化对象那部分的代码,无限闪退。
找到原因的办法:logcat大法!!!!闪退的时候帮我直接定位到了多余的那一行,我就揪出了元凶。

复现闪退情况,logcat直接给我指出来多少行出了什么错误

2.导出为APK,手机安装证书出错

也是反反复复安装失败

新建无数个证书都没用,最后发现选错了选项,如下图:

这里直接选择v1就能生成完整带加密的APK了,如果选V2,出错得你怀疑人生

]]>
Android
<![CDATA[校园网电信宽带在线受理通道]]>https://ariser.cn/index.php/archives/12/饭可以不吃,网能不上?

校园网近期逐步降速,四月十号全面断网

湖南科技大学电信宽带网上申请预订通道⇐戳

湖南科技大学使用运营商网络指南⇐戳

校园网提质服务中心⇐戳

或者扫描下列二维码:

  • 具体资费如下:

盗图的请注明出处,或者也稍微根据资费P一下啊/笑哭

各套餐详解(长按以查看)

另移动联通也能找我办理啊,详情点击博客首页(有点慢,正在检修),下方的QQ、微信、微博都能找到我哦!

]]>
Others
<![CDATA[基于Web的高校占教系统(解决方案)]]>https://ariser.cn/index.php/archives/13/

把想法说出口,和团队一起讨论、进行开发,虽然道阻且艰,但其过程妙不可言

问卷调查入口

前台:

  • 查询
    1.空教室

    • 按日期查询
    • 按教室

      1. 南北校
      2. 楼群
        c.教室号

2.教室信息
(1)可租用时间
(2)功能(多媒体、空调)
(3)容量

  • 申请
    1.预约教室

提交页面表单:
(1)姓名学号由教务网数据库自动导入
(2)联系方式
(3)活动类型
a.班级活动(团活、班会、联谊)
填入班级,联谊可填入多个班级
b.部门活动
填入部门名称
c.社团活动
填入社团名称
d.校内外培训组织
填入组织名称
e.院级、校级活动(高级用户可选)
(4)申请简介
2.使用多媒体功能,接口

  • 用户信息查询
    预约管理(查询+推选+修改)

被驳回的处理

  • 使用指南+公告
    (校院级别大型活动占用教室,及时通知占教的用户)
  • 反馈入口(BUG, 意外情况)

后台:

一、查询、修改、删除
二、审核
三、导出到excle表、方便校院分析各班团学习、生活组织活动
四、与教务网(现代教育信息对接)
五、突发情况(校院会议)的教室申请,给与用户通知
六、学院每学期前提交晚自习的情况,(早晚自习占用的教室,在教务网和公众号上查询不到,这个得加入进去)

后台(权限)
查询 申请 审核 修改 驳回 系统权限

游客 √ X X X X X

学生 √ √ X √(自己) X X

高级用户 √ √ √ √ √ X

审核用户 √ X √ √ √ X

管理员 √ √ √ √ √ √

注:
1.高级用户(辅导员及以上)可以在已申请的教室上复占
2.高级用户及管理员可以添加晚自习内容
3.审核用户,需要分配校院学习部、校自理的人员来参与承担这个任务
4."自己"是指:普通用户(学生)只能修改自己的申请记录,且学生用户一次只能申请一间教室

解决的痛点:

一、费时费力,需要占教人实地一间间教室看
二、被擦掉的占教通知,会被复占,引起误会
三、黑板、门上留下的粘胶难以处理,永久性损坏
四、黑板上的占教通知影响老师上课

解决方案:

让占教简单化,如同看一场电影,购票选座。

实现:前台:HTML(包含CSS+JS) + JSP
后台:数据库,Mysql
开发工具:Eclipse/My eclipse
运行环境:tomcat
运用MVC框架(模型+视图+控制器),这个MVC框架自行百度百科,写入到报告书

数据库模型:

教室表(教室号,容量,功能,管理员,状态)
租用时间表(编号,时间,教室号)
学生表(姓名,学号,所属院系,联系方式)
预定信息表(预订编号,周数,学号,编号)
管理员表(工号,姓名,联系方式)
状态:00空闲 01有课 10占用

安全性:

学生登陆通过教务信息系统验证,对异常提交或提交无关内容的用户进行处理

创新点:

  1. 在线预约
  2. 给校院提供学生、班团活动开展的数据
  3. 响应式+多接入点(web+微信+小程序)
  4. 早晚自习数据导入,完善旧教务查课的功能
  5. 开源(仅提供给校内开发者,技术分享+功能完善)
    ps:因为如果项目发展到校外,要拿来卖钱
  6. 结合学生兴趣爱好,进行大数据分析,智能匹配教室

项目发展:

  1. demo(以五教为例),实现占教、审核等功能
  2. 功能初步完善,开启内测,提供给校方审核,逐步完善
  3. 交由校知名网络安全团队进行安全评估
  4. 校方审核通过,整体发通知,在线占教施行,逐步成为共识
  5. 给其它高校提供占教解决方案(定制)
  6. 超级占教助手上线(类似超级课程表),并接入更多功能
  7. 结合学生兴趣爱好,进行大数据分析,智能匹配教室(使项目具有学术价值,具体方案择日开会讨论)
]]>
Others
<![CDATA[K2刷潘多拉-认证哆点Drcom]]>https://ariser.cn/index.php/archives/14/

闲来无事,把寝室的K2拿来刷潘多拉玩玩,看能不能实现路由器内部验证,实现多设备用一个校园网账号上网。
简单原理:刷入开源的PanduoraBox,里面运行python程序模拟验证过程。

声明:只作为技术研究交流,不要用于其它非法途径,这个东西学校会封禁的。

刷潘多拉

  • 给路由器刷入Breed(类似PC的BIOS)

用Breed工具一键刷入

拔掉电源->按住reset键不松-十秒左右>插上电源->地址栏输ip以进入
(192.168.1.1和.2.1都试一下,因为可能变动)

  • 进入Breed刷潘多拉固件

重要:环境变量 -> 禁用 -> 设置
再可以在固件更新里面选择panduora固件更新了
(没有设置禁用环境变量会无法重启,一直进入breed)

原因在此,breed版本原因。
在这个地方卡了好大半天

加载完毕后等待或者手动重启路由器即可

如果进入这个页面,代表潘多拉已经刷成功了!

到这个地方,我已经睡觉了,后面的步骤还没开始。第二天早上,室友连接上去就可以直接上网,不需要验证了????

有点迷,在自己电脑上验证一次之后,其他设备直接接入,无需验证。

Fing界面,接入了五台设备

  • 刷Python脚本

接着往下,安装python2.7。
菜单中的Add python.exe to Path一定要选上。

打开控制台,输入python,如果出现版本信息,代表成功安装。

停更一段落,手头任务太多了。

]]>
Others
<![CDATA[实验二 Linux内核实验]]>https://ariser.cn/index.php/archives/15/

目录

  • 准备工作

    • SSH工具控制Linux
    • 下载Liunx内核
  • 编译系统内核

    • 配置内核
    • 编译内核
    • 清除文件
  • 增加模块

准备工作

  • 虚拟机系统:CentOS5.6,配置SSH
#安装SSH:
yum install openssh-sever SSH
#服务重启:
service sshd restart 
#设置SSH为开机自启:
chkconfig sshd on 
#查看是否安装SSH:
rpm -qa | grep ssh 
#查看是否运行SSH:
/etc/init.d/sshd status

  • 用SSH工具连接
    查看IP:ifconfig (Windows下是ipconfig)


X SHELL连接界面(SSH工具还有:putty等。手机上:JuiceSSH)

  • 下载Linux内核:
    下载的时候,校园网走的是移动的线路,封掉了很多ip,速度是奇慢无比


手机热点 + IDM,速度蹭蹭得往上飙

将下载好的内核复制到了root目录下。


编译内核系统

内核版本 2.6.32

解压压缩包,到/usr/src下:tar -xvf linux-3.16.56.tar.xz -C /usr/src (注意按TAB自动补齐)
文件夹重命名为学号:mv linux-3.16.56 1601020306

安装QT,过程中出现了各种各样的错误,也换了几种方法,参考:

  • 配置内核(根据需求自定义选择模块)
    进入到内核目录,执行 make menuconfig,发现出错了


yum install -y ncurses-devel 安装了一个东西,暂时不知道是什么左右,再运行make menuconfig

这么多选项,要知道哪个需要那个不需要不是容易事。这里找到自己Linux的配置,导入进去

cd到boot文件夹下
cp到内核根目录下

选择,填写文件名,返回第一个界面,选择,命名位.config后就
(这里make xconfig是一种全图形界面的配置方式,需要用到上文安装的QT的共享库)

  • 编译内核
    漫长的过程,我就去睡觉了,第二天早上来收菜QAQ。

  • 安装内核
    编译完成,至于时间,没有记录,一觉醒来电脑都自己进入休眠了


执行make modules_install和make install

打开编辑内核启动项的配置文件:
vim /boot/grub/grub.conf,默认0开始计数,因此需将default的☞改为0即可

查看内核信息,已经是3.16.56内核了

make distclean,清除内核安装后留下的痕迹。


添加模块

cd到lib/modules/(任意目录即可)
自定义的hello模块

创建了模块hello以及对应的makefile

这个过程持续了一个小时多,改了无数遍Makefile,各种错误,无果,暂时还在解决,最后的运行结果以及解决方案会更新到博客里面。

]]>
Linux
<![CDATA[Java - 初试]]>https://ariser.cn/index.php/archives/16/

Java开发环境配置

  • 下载安装JDK
    oracle官网,找到对应版本JDK下载,传送门

  • 配置环境变量
    右键计算机 -> 属性 -> 高级系统设置 -> 环境变量


新建一个变量名为“JAVA_HOME”的系统变量,变量值为“E:DevelopmentJavajdk1.8.0_92”(jdk的安装目录,根据个人所安装的目录修改)

再新建一个变量名为“CLASSPATH”,变量值为“.;%JAVA_HOME%libdt.jar;%JAVA_HOME%libtools.jar;”的系统变量,注意前面的点号和分号都是有的

打开“Path”系统变量,点击新建,添加“%JAVA_HOME%bin”和“%JAVA_HOME%jrebin”两个系统变量。Path使得系统可以在任何路径下识别java命令

验证是否配置好环境:window+R输入cmd,打开控制台或者开始-运行
分别输入java和java -version和javac,都正常运行即代表java已经正确安装,其中 java -version代表你安装的java的版本 。
如果出现类似"javac不是内部或外部命令"等提示语句,请再三检查你的环境变量是否正确配置,或者JDK和JRE安装目录是否重复,如果重复,可以选择再次运行下载的jdk安装程序重新安装。

  • 安装编译器
    安装Eclipse编译器,开始Java编程。

冒泡排序

老师留下的作业是用Java实现冒泡排序

核心算法为:外层循环遍历数组 -> 内层循环进行交换(小的在前大的在后)

实现代码:

for(int i=1; i&lt;=b.length; i++)
{
    for(int j=1; j&lt;=b.length-i; j++) { if(b[j-1] &gt; b[j]) {
        int x = b[j-1];
        b[j-1] = b[j];
        b[j] = x;
        }
    }
}

整个Java代码片段为:

程序优化升级

  1. 加入输入功能
    在第一个版本的基础上,结合网上搜到的知识,实现了输入任意数量的数字进行排序:


思路: 首先输入字符串的数组,中间用空格分隔,然后获取字符串长度,作为int[] 初始数组的长度,再将String字符串数组转化为int数组。

Eclipse运行结果如图,结合了上述方法,实现了任意长度数组的冒泡排序(长按图片以查看)

  1. 封装到类
    结合学习的面向对象知识,进一步改造:
]]>
Java
<![CDATA[实验一 Linux系统安装及操作]]>https://ariser.cn/index.php/archives/17/

目录

  • 虚拟机安装
  • Linux Mint镜像下载及安装
  • Linux Mint系统的基本操作及任务完成(C代码的编译)

虚拟机安装

平时使用虚拟机比较多,用于学习网络Web知识、Kali渗透测试、网站搭建、测试木马病毒以及体验一些镜像(凤凰OS、MacOS、Ubtun等)。
版本:Vmware workstation 12 pro,使用的注册机激活的。

安装过程在此不作赘述,另外分享几点心得:

  1. Vmware开机是自动启动相关服务,这里可以在服务里面设置为手动,在使用Vmware的时候手动在任务管理器里面开启,以避免浪费系统资源。
    任务管理器
  2. 所安装的Window10系统是Insider preview。预览版由于自动开启了Hyper-V(微软自带的虚拟机产品),导致Vmware出现“VMware Workstation and Hyper-V are notcompatible. Remove the Hyper-V role from the system before running VMwareWorkstation.”的报错,这里的话,到程序和面板设置关闭Hyper-V服务。
    二者共存的方法

Linux Mint镜像下载及安装

进入到Linux Mint的官网,下载页面有三种版本,百度了一下是其桌面系统的区别,这里选择第一种Cinnamon的64位。
Linux Mint官网
官网给的下载地址来自“清华大学开源软件镜像站”
分配了双核2G
安装虚拟机工具的时候,挂载镜像的时候发现没有操作权限,终端显示的$证明不是超级用户,于是:

#卸载桌面上的工具镜像
umount /dev/sr0
#挂载到media
mount /dev/sr0 /media
#进入到media文件夹 ls 查看文件 tar -zxvf解压文件到/root文件夹
cd /media


重启后,分辨率变得很舒服了。


Linux Mint系统的基本操作及任务完成(C代码的编译)

终端安装ATOM编译器的过程比较漫长,在安装的过程中用vim编辑了C文件:

#vim编辑器常见命令
a: (光标后)插入 A: 行尾插入
O: 上一行插入 u: 恢复
i: (光标前)插入 R: 行首插入
p: 粘贴 dd: 剪切当前行(2dd是剪切两行)
Esc退出后输入冒号,q: 退出,w: 保存

gcc命令,编译链接得到exe文件,执行,得到任务结果

中间碰到的一些其它问题会单独写到其它文章。

初学Linux,建议建立一个记事本,把Linux的命令都记录起来,每天训练,达到如同操作图形界面一样熟练。

]]>
Linux
<![CDATA[【剑指Offer】T51 数组中的逆序对]]>https://ariser.cn/index.php/archives/406/

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007

输入描述:

题目保证输入的数组中没有的相同的数字数据范围:    对于%50的数据,size<=10^4    对于%75的数据,size<=10^5    对于%100的数据,size<=2*10^5

思路1:归并排序思想,其实就是调整逆序对的过程。可以描述为:分组到最小(两个一组),然后调整逆序对。合并成四个再调整逆序对直到合并成原始数组。所以在这个过程中的调整次数即逆序对的个数。O(nlog2n)

先默写一遍归并排序:

// 归并过程
void merge(int A[], int low, int mid, int high){
    int help[high - low + 1]; // 辅助数组(保护现场,以便后面继续,整个做完后再赋给原数组对应的部分)
    int i = 0;
    int lowIndex = low;
    int highIndex = mid + 1;
    
    while(lowIndex <= mid && highIndex <= high){
        if(A[lowIndex] < A[highIndex])
            help[i++] = A[lowIndex++];
        else
            help[i++] = A[highIndex++];
    }
    
    //左边和右边肯定有一边到头了,不可能同时,因为每次只移动一边
    while(lowIndex <= mid)
        help[i++] = A[lowIndex++];
    while(highIndex <= high)
        help[i++] = A[highIndex++];
    
    //将排好序的辅助数组赋值给原始数组,不需要返回值
    for(int i = 0; i < high-low+1; i++){
        A[low+i] = help[i];
    }
}

//递归
void mergeSort(int A[], int low, int high){
    if(low == high)
        return;
    
    int mid = low + (high-low)/2;
    
    // 左部分归并排序
    mergeSort(A, low, mid);
    // 右部分归并排序
    mergeSort(A, mid+1, high);
    // 左右部分归并
    merge(A, low, mid, high);
}

//重写, 归并整个数组
void mergeSort(int A[], int n){
    if(A == NULL || n < 2)
        return;
    mergeSort(A, 0, n-1);
}

int main(){
    int n; 
    while(cin >> n){
        int arr[n];
        for(int i = 0; i < n; i++) cin >> arr[i];
 
        mergeSort(arr, n);
 
        for(int i = 0; i < n; i++){
            cout << arr[i] << " ";
        } 
        cout << endl;
    }
    return 0;
} 

Vector版:

#include <iostream>
#include <stdio.h>
#include <vector>
#include <map>
#include <stack>
#include <queue>
using namespace std;

void merge(vector<int>& A, int low, int mid, int high){
    int i = 0;
    
    int help[high - low + 1];
    int lowIndex = low;
    int highIndex = mid + 1;
    
    while (lowIndex <= mid && highIndex <= high) {
        if(A[lowIndex] <= A[highIndex])
            help[i++] = A[lowIndex++];
        else
            help[i++] = A[highIndex++];
    }
    
    while (lowIndex <= mid)
        help[i++] = A[lowIndex++];
    
    while (highIndex <= high)
        help[i++] = A[highIndex++];
    
    for (int i = 0; i < high-low+1; i++) {
        A[low+i] = help[i];
    }
}

void mergeSort(vector<int>& A, int low, int high){
    if(low == high)
        return;
    
    int mid = low + (high-low)/2;
    printf("%d\n", A[mid]);
    mergeSort(A, low, mid);
    mergeSort(A, mid+1, high);
    merge(A, low, mid, high);
}

void mergeSort(vector<int>& A){
    int n = A.size();
    if(A.empty() || n < 2)
        return;
    
    mergeSort(A, 0, n-1);
}

void PrintArr(vector<int> arr){
    for (int i = 0; i < arr.size(); i++) {
        if(i == arr.size()-1)
            cout<<arr[i]<<endl;
        else
            cout<<arr[i]<<", ";
    }
}

void PrintArr(int arr[], int n){
    for (int i = 0; i < n; i++) {
        if(i == n-1)
            cout<<arr[i]<<endl;
        else
            cout<<arr[i]<<", ";
    }
}

int main() {
    vector<int> A = {6, -3, -2, 7, -15, 1, 2, 2};
    mergeSort(A);
    PrintArr(A);
    
    return 0;
}

// 未完待续

思路2:树状数组

]]>
算法题
<![CDATA[【剑指Offer】T50 第一个只出现一次的字符(哈希)]]>https://ariser.cn/index.php/archives/404/

请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。

如果当前字符流没有存在出现一次的字符,返回#字符。

思路:同样是哈希,只是可以直接用数组代替哈希,映射函数即为字符的值

class Solution
{
public:
    string s;
    char hash[256] = {0};
  //Insert one char from stringstream
    void Insert(char ch){
        s+=ch;
        hash[ch]++;
    }
  //return the first appearence once char in current stringstream
    char FirstAppearingOnce(){
        int i = 0;
        while(i < s.size()){
            if(hash[s[i]] == 1)
                return s[i];
            i++;
        }
        return '#';
    }

};
]]>
算法题
<![CDATA[【剑指Offer】数组中重复的数字(哈希)]]>https://ariser.cn/index.php/archives/403/

在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

思路:利用Map,记录每个值,若已经存在,则跳出。

bool duplicate(int numbers[], int length, int* duplication) {
    map<int, int> _map;
    for (int i = 0; i < length; i++) {
        if(_map[numbers[i]] == 1){
            *duplication = numbers[i];
            return true;
        }
        _map[numbers[i]]++;
    }
    return false;
}
]]>
算法题
<![CDATA[【剑指Offer】T65 不用加减乘除实现加法]]>https://ariser.cn/index.php/archives/402/

写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。

思路:以 87 + 15 = 102 为例

  • 正常加法拆分

    • 87 = 1010111(2),15 = 1111(2) 得到 102 = 1100110(2)
    • 不进位加法 1011000
    • 产生进位后的结果 0011100(作减法或者自己推导)
  • 使用二进制实现

    • 不进位的加法,显然就是 异或运算
    • 关键是求进位后的结果
    • 将上述两个结果相加,即可得到最终结果
    • 进位后结果 可以描述为:

      • 除了1加1都不会产生进位,可以想象成两个数先位与再左移一位(当前位置的1左移到进位的位置)。
      • 只有当两个数都为1的时候,位于结果为1,其余为0
      • 第一轮

        • 1010111 ^ 1111 = 1011000
        • (1010111 & 1111) << 1 = 0111 << 1 = 1110
      • 第二轮

        • 1011000 ^ 1110 = 1010110
        • (1011000 & 1110)<<1 = 10000
      • 第三轮

        • 1010110 ^ 10000 = 1000110
        • (1010110 & 10000)<<1 = 100000
      • 第四轮

        • 1000110 ^ 100000 = 1100110
        • (1000110 & 100000)<<1 = 0000
      • 跳出循环,结果为 1100110 + 0000 = 1100110

代码:

int Add(int num1, num2){
    int sum, carry;
    do{
        sum = num1 ^ num2;
        carry = (num1 & num2)<<1;
        num1 = sum;
        num2 = carry;
    }while(num2 != 0);
    return num2;
}
]]>
算法题
<![CDATA[【剑指Offer】把二叉树打印成多行]]>https://ariser.cn/index.php/archives/400/

从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。

思路:借助队列,每次打印完当前行,就把他们的子节点入队。队不空的时候一直做判断

由于每一行分开打印,所以每次外层循环时,算作打印一行,要记录队列中结点个数,然后依次存入数组中后再存入总结果。

class Solution {
public:
        vector<vector<int> > Print(TreeNode* pRoot) {
            vector<vector<int> > res;

            if(!pRoot)
                return res;

            queue<TreeNode*> que;

            que.push(pRoot);
            while (!que.empty()) {
                int low = 0, high = que.size();
                vector<int> row;
                while (low++ < high) {
                    TreeNode* q = que.front();
                    que.pop();
                    row.push_back(q->val);
                    if(q->left)
                        que.push(q->left);
                    if(q->right)
                        que.push(q->right);
                }
                res.push_back(row);
            }
            return res;
     }
};
]]>
算法题
<![CDATA[【剑指Offer】数组中只出现一次的数字]]>https://ariser.cn/index.php/archives/397/

一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

思路1:利用Map存储次数,最后遍历map拿出次数为1的数字

class Solution {
public:
    void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
            map<int, int> _map;
            for (int i = 0; i < data.size(); i++) {
                _map[data[i]]++;
            }
            vector<int> nums;
            map<int, int>::iterator iter = _map.begin();
            while (iter != _map.end()) {
                if(iter->second == 1)
                    nums.push_back(iter->first);
                iter++;
            }
            *num1 = nums[0];
            *num2 = nums[1];
    }
};

思路2:位运算的性质

  • 数字和自己本身异或为0(相同为0,不同为1)(&相同为1不同为0,不进位)
  • 0和任何数字异或为这个数字

所以先将所有数字异或可以得到最后两个数字异或的结果,再从低往高找差异位,再进行分组。

1, 1,2,2,3,4,5,5

001,001,010,010,011,100,101,101

全部异或得到:111,最低位为第1(即 两个数从此出现差异)。3和4分别最低位为1和0,正好分到了xx1和xx0两个组,分组,组内再异或即可得到3 和 4。

class Solution {
public:
    // 为1的最高位
    // 右移的过程中判断最低位
    int findFirst1(int bitResult){
        int index = 0;
        while(index < 32 && ((bitResult & 1) == 0)){
            bitResult >>= 1;
            index++;
        }
        return index;
    }
    
    // 某位是否为1
    bool isBit1(int target, int index){
        return ((target >> index)&1) == 1;
    }
    
    void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
        int bitResult = 0;
        for(int i = 0; i < data.size(); i++){
            bitResult ^= data[i];
        }
        // 得到 num1 和 num2 异或的结果,求最高位1的位置1001为4
        int index = findFirst1(bitResult);
        // 分组,最高位为1和不为1的分组,异或之后的结果
        for(int i = 0; i < data.size(); i++){
            if(isBit1(data[i], index))
                *num1 ^= data[i];
            else
                *num2 ^= data[i];
        }
    }
};
]]>
算法题
<![CDATA[【LeetCode】T56 合并区间]]>https://ariser.cn/index.php/archives/394/

给出一个区间的集合,请合并所有重叠的区间。

示例 1:

输入: [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

特殊的用例:
[3,4][1,0] (后面的会插到前面或中间,不是简单的末尾合并)
[1,4][2,3] (后面的会并入前一个里面)

思路:创建res,每次往里面插入一个新的区间,然后和res尾部的合并(最后一点有优化)

  • [1][4] [4][5]首先就是最简单的并入

    • res[len-2][1] = res[len-1][1];
  • [1,4][2,3]后面的直接并入前一个

    • res[len-2] = res[len-1];
  • 提交了一次后GG,出错的用例如下:

    • [3,4][1,0](后面的会插到前面或中间,不是简单的末尾合并)
  • 于是思考了一下如何插入到中间,巴拉巴拉半天想想,觉得太复杂了!看了下评论区,看到了先排序的方法,恍然大悟。
class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        if(intervals.size() < 2)
            return intervals;

        // 每次往res内添加一组区间,动态维护res,而不在原数组上维护。
        vector<vector<int>> res;
        // 必须先排序,否则res尾部插入[0,1]这种 “会在中间合并的数组”,会异常麻烦
        sort(intervals.begin(), intervals.end());
        
        res.push_back(intervals[0]);

        // 每次往res尾部推入一个区间,有合并就改变末尾推出一个
        for (int i = 1; i < intervals.size(); i++) { // [][]
            res.push_back(intervals[i]);
            int len = res.size();
            // 合并操作
            if(res[len-1][1] <= res[len-2][1]) // [1,4][2,3],只比较后面因为前面已经排序排好了
                res.pop_back();
            else
                if(res[len-1][0] <= res[len-2][1]){
                    if(res[len-1][0] <= res[len-2][0]){// [2,3][2,4]
                        res[len-2] = res[len-1];// 倒数第二替换为倒数第一
                        res.pop_back();
                    }
                    else{ // [1,3][2,4] 正常k合并
                        res[len-2][1] = res[len-1][1];
                        res.pop_back();
                    }
                }
        }
        return res;
    }
};
]]>
算法题
<![CDATA[【LeetCode】T43 1~n整数中1出现的次数]]>https://ariser.cn/index.php/archives/393/

输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

aab 、"alouzxilkaxkufsu"(leetcode有栈溢出情况)

思路:快慢指针,慢指针停留,快指针往前,并往map里面记录字符的次数(只要没重复的就往前)。若出现重复则低指针到高指针位置,清空map。整个过程更新最大值。

  • 维护一个map{a, 1},数字代表出现次数;max为最大长度,t为每轮的长度
  • 快慢指针hl,快指针往前,用s[h]的值在map里面查

    • 没有重复就前进,在map中标记s[h],更新maxt
    • 有重复,慢指针l 移到 h处,h前进,更新maxt;清空map

奈何长度过长时,会出现缓冲区溢出的情况,本地测试通过。

int lengthOfLongestSubstring(string s) {
    if(s.length() == 0)
        return 0;
    
    map<int, int> _map;
    int low = 0;
    int high = 0;
    int t = 0;
    int max = 0;
    while(low < s.size() && high < s.size()){
        if(_map[s[high]] == 0){
            if(s[high] == s[high -1]) // aab的情况
                t = 1;
            else
                t++;
            max = t > max ? t : max;
            _map[s[high]] = 1;
            high++;
        } else {
            max = t > max ? t : max;
            t = (s[high] == s[high -1]) ? 1 : 0; // aab的情况
            low = high;
            high++;
            _map.clear();
        }
    }
    return max;
}

最后借鉴了评论区的思路,不用记录每一轮的 t,用高低指针之差算长度。后面调试过程发现应该是由_map.clear();引起的,还是循环的过程中 _map[s[low]]--

int lengthOfLongestSubstring(string s) {
    if(s.length() == 0)
        return 0;

    map<int, int> _map;
    int low = 0;
    int high = 0;
    int max = 0;
    while(high < s.size()){
        if(_map[s[high]] == 0){
            _map[s[high]]++;
            high++;
            max = max > (high - low) ? max : (high - low);
        } else {
            max = max > (high - low) ? max : (high - low);
            _map[s[low]]--;
            low++;
        }
    }
    return max;
}
]]>
算法题
<![CDATA[【剑指Offer】T43 1~n整数中1出现的次数]]>https://ariser.cn/index.php/archives/392/

求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。

思路1:遍历整个数组,并除10求余数。

int NumberOf1Between1AndN_Solution(int n){
    int sum = 0;
    for(int i = 1; i <= n; i++){
        int sumOfOne = 0;
        int k = i;
        while(k){
            if(k % 10 == 1)
                sumOfOne++;
            k = k / 10;
        }
        sum+=sumOfOne;
    }
    return sum;
}
]]>
算法题
<![CDATA[【剑指Offer】T42 连续子数组最大和]]>https://ariser.cn/index.php/archives/391/

HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

思路1:累加,更新最大值

  • sum大于max,更新max
  • sum小于maxmax不变
  • 记得初始化的时候,max应该设置为A[0]或最小值0x80000000。若按照平常设置0,全负的时候会把0当做最大
class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        if(array.size() == 1)
            return array[0];
        
        // max必须从第一值取起,不然会导致全负的时候,输出0
        int max = array[0];
        int sum = 0;
        
        for(int i = 0; i < array.size(); i++){
            if(sum < 0)
                sum = array[i];
            else
                sum += array[i];
            
            if(sum > max)
                max = sum;
        }
        return max;
    }
};

思路2:动态规划,其实代码和上面的一模一样,思维模式的变化:从第一个累加和不为负数开始,进行更新最大值。

]]>
算法题
<![CDATA[【剑指Offer】T41 数据流中的中位数]]>https://ariser.cn/index.php/archives/390/

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

思路:

  • 借助两个堆,大根堆max和小根堆min

    • 大根堆存放中位数左边的数字
    • 小根堆存放中位数右边的数字
  • 需要满足:大根堆堆顶(最大) <= 小根堆堆顶(最小)
  • 存入的时候维护堆,取出的时候要么是两堆顶平均数要么是大根堆顶
class Solution {
    priority_queue<int, vector<int>, less<int> > max;
    priority_queue<int, vector<int>, greater<int> > min;
public:
    void Insert(int num){
        if(max.empty() || num <= max.top())
            max.push(num);
        else
            min.push(num);
        
        // 控制max.size()大于min.size()且不超过2
        // 原因是,max堆内是从 最小值 增长到 中位数,两堆堆相等时取平均值,相差1时取大堆顶
        if(max.size() - min.size() == 2){
            // max堆顶到min
            min.push(max.top());
            max.pop();
        }
        
        if(min.size() - max.size() == 1){
            // min堆顶到max
            max.push(min.top());
            min.pop();
        } 
    }

    double GetMedian(){ 
        // 两堆堆相等时取平均值,相差1时取大堆顶
        return max.size() == min.size() ? (max.top() + (min.top() - max.top())/2.0) : max.top();
    }
};
]]>
算法题
<![CDATA[【剑指Offer】T40 最小的K个数]]>https://ariser.cn/index.php/archives/389/

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。

思路1:快排思想,对原数组进行调整

  • 选取A[0]作枢纽值index,调整
  • 当枢纽index不为k-1 时,继续调整(数组下标0开始)

    • index大于 k-1 ,高指针下移,调整
    • index小于 k-1 ,低指针上移,调整
  • 调整后截取 0k,即为最后结果

代码:

class Solution {
public:
    int Partition(vector<int>& A, int low, int high){
        
        int pivot = A[low];
        while(low < high){
            while (low < high && A[high] >= pivot)
                high--;
            A[low] = A[high];
            while(low < high && A[low] <= pivot)
                low++;
            A[high] = A[low];
        }
        A[low] = pivot;
        return low;
    }
    
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        int len = input.size();
    
        if(len == 0 || k > len || k <= 0)
            return vector<int>();
        if(len == k)
            return input;

        int low = 0;
        int high = len - 1;
        int pivot = Partition(input, low , high);
        while(pivot != k-1){
            if(pivot > k-1){
                high = pivot - 1;
                pivot = Partition(input, low, high);
            }else{
                low = pivot + 1;
                pivot = Partition(input, low, high);
            }
        }
        vector<int> res(input.begin(), input.begin() + k);
        return res;
    }
};

快排应该信手拈来

思路2:全排序,直接用sort(),再输出前 k

vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
    if(input.empty() || k > input.size())
        return vector<int>();
    
    sort(input.begin(), input.end());
    vector<int> res(input.begin(), input.begin()+k);
    return res;
}
]]>
算法题
<![CDATA[【剑指Offer】T39数组中出现次数超过一半的数字]]>https://ariser.cn/index.php/archives/388/

T39数组中出现次数超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

思路1:评论区看到的一种“投票算法”,即票数过一半者当选。

  • 设置票数 ticket 和 当选者 majority,遍历数组过程进行演变

    • A[i] 等于 majority,票数加1
    • A[i]不等于 majority,票数减1
    • 当票数为0时,更换当选者
  • 如果有到最后票数大于1,那么当选者可能为要求的数字

    • 可以手动演变,只要数字大于总数的一半,最后当选者肯定会留下。
  • 再遍历一遍,统计数目,大于 len/2 即可认为符合要求,否则输出0

代码:其中最后一个 for(int number : numbers){}可以当作foreach

int MoreThanHalfNum_Solution(vector<int> numbers) {
        int len = numbers.size();

        if(len == 0)
            return 0;

        int ticket = 1;
        int majority = numbers[0];

        for(int i = 1; i < len; i++){
            if(numbers[i] == majority)
                ticket++;
            else
                ticket--;

            if(ticket == 0){
                majority = numbers[i];
                ticket = 1;
            }
        }

        ticket = 0;
        for(int number : numbers){
            if(number == majority)
                ticket++;
        }

        return ticket > len / 2 ? majority : 0;
    }

思路2:利用Map,空间换时间。注意导入头文件

#include <map>
int MoreThanHalfNum_Solution(vector<int> numbers) {
    int n = numbers.size();
    map<int, int> m;
    int count;
    for(int i = 0; i < m; i++){
        count = m[number[i]]++;
        if(count > n/2)
            return number[i];
    }
    return 0;
}
]]>
算法题
<![CDATA[【剑指Offer】T36 二叉搜索树和双向链表]]>https://ariser.cn/index.php/archives/387/

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

思路:思路借鉴二叉树的中序线索化(二叉排序树中序序列为有序)

  • 线索化函数 helper

    • 左子树线索化
    • 进行连接

      • p->left指向pre
      • pre不为空时,指向p
      • p地址到pre以便下一次递归
    • 右子树线索化
  • 主函数调用递归函数
  • 返回最左节点(调用线索化函数后直接返回并不是完整的序列)
  • 需要注意的是pre必须定义为应用类型,不然下次递归并不会和上次递归有连接
/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};*/

class Solution {
public:
    void Helper(TreeNode* node, TreeNode* &pre){
        if(!node)
            return nullptr;
        
        // 左子树线索化
        Helper(node->left, pre);
        
        // 线索化过程
        node->left = pre;
        if(pre)
            pre->right = node;
        pre = node;
        
        Helper(p->right, pre);
    }
    
    
    TreeNode* Convert(TreeNode* pRootOfTree){
        if(!pRootOfTree)
            return nullptr;
        // 第一个pre肯定为空
        TreeNode* pre = nullptr;
        Helper(pRootOfTree, pre);
        
        // 找到最左节点
        TreeNode* list = pRootOfTree;
        while(list->left)
            list = list->left;
        return list;
    }
}
]]>
算法题
<![CDATA[【剑指Offer】T35 复杂链表的复制]]>https://ariser.cn/index.php/archives/385/

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)

思路:复制,设置指针,分裂。示意图见P187

分裂的那一步出错了,没有Clone = Clone->next;

/*
struct RandomListNode {
    int label;
    struct RandomListNode *next, *random;
    RandomListNode(int x) :
            label(x), next(NULL), random(NULL) {
    }
};
*/
class Solution {
public:
    // 克隆
    void CloneNodes(RandomListNode* pHead){
        RandomListNode* p = pHead;
        while(p){
            // 没有判断p->next是因为没有对p->next取值,所以可以操作
            RandomListNode* clone = new RandomListNode(0);
            clone->label = p->label; 
            clone->next = p->next; 
            clone->random = nullptr;
            p->next = clone;
            p = clone->next;
        }
    }
    
    // 设置Random指针
    void SetRandom(RandomListNode* pHead){
        RandomListNode* p = pHead;
        while(p){
            if(p->random != nullptr)
                p->next->random = p->random->next;
//            else
//                p->next->random = nullptr;
            // 上两行没必要,因为本来就是指向空
            p = p->next->next;
        }
    }
    // 分裂
    RandomListNode* Split(RandomListNode* pHead){
        RandomListNode* p = pHead;
        // 设置头结点的思想,让游标节点去执行连接工作,最后返回头结点
        RandomListNode* clone = nullptr;
        RandomListNode* cloneHead = nullptr;
        
        
        if(p){
            clone = pHead->next;
            cloneHead = clone;
            p->next = clone->next;
            p = p->next;
        }
        
        while(p){
            clone->next = p->next;
            clone = clone->next;
            p->next = clone->next;
            p = p->next;
        }
        return cloneHead;
    }
    
    RandomListNode* Clone(RandomListNode* pHead){
        CloneNodes(pHead);
        SetRandom(pHead);
        return Split(pHead);
    }
};

分裂操作中,设置头结点思想用过很多次。开始设置头结点和游标节点指向“头结点地址”,让游标节点往后走执行连接操作,操作完成后,返回的头结点即整个链表或树。

]]>
算法题
<![CDATA[【剑指Offer】T34 二叉树中和为某一值的路径]]>https://ariser.cn/index.php/archives/384/

输入一颗二叉树的根节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前)

思路:利用先序遍历进行探路

  • 访问节点,添加到路径内,计算累加值
  • 如果是叶节点

    • 累加值符合要求:打印
    • 不符合要求,删最后的叶节点,访问 父节点的另外一个孩子,不符合的话继续回溯
  • 借助栈来实现(题目给的是vector,pop_back是最后一个元素,可以代替栈)
class Solution {
public:
    int num;
    vector<int> path;
    vector<vector<int> > res;
    void finder(TreeNode* root, int expectNumber){
        // 空,递归出口
        if(!root)
            return;
        
        num += root->val;
        path.push_back(root->val);
        
        // 符合条件,路径存入
        if(!root->left && !root->right && num==expectNumber)
            res.push_back(path);
        
        //继续探路
        finder(root->left, expectNumber);
        finder(root->right, expectNumber);
        // 探到叶节点,回溯
        path.pop_back();
        num-=root->val;
    }
    vector<vector<int> > FindPath(TreeNode* root, int expectNumber) {
        finder(root, expectNumber);
        return res;
    }
};
]]>
算法题
<![CDATA[【剑指Offer】T33 判断是否是二叉搜索树的后序序列]]>https://ariser.cn/index.php/archives/382/

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。

思路:

  • 例如 5、7、6、9、11、10、8

    • 二叉树后序序列,最后一个数字为根节点值
    • 前三个小于8,后三个大于8,分别为左子树和右子树。子序列内也满足,6为根,5小于,7大于。
  • 再如 7、4、6、5

    • 5为根节点
    • 7大于5,可能没有左子树
    • 但是右子树上却右4小于5,不满足右子树大于根节点,不符合
  • 故找到根节点,再往后遍历

    • 都小于根的序列定义为左子树序列
    • 再判断右边序列是否都大于根节点
    • 递归
class Solution {
public:
    bool VerifySquenceOfBST(vector<int> sequence) {
        int len = sequence.size();
        if(len == 0)
           return false;
        if(len == 1)
            return true;
        int gen = sequence[len-1]; // 根节点值
        
        vector<int> left;
        vector<int> right;
        int i; // 左子树序列,都小于根节点
        for(i = 0; i < len, sequence[i] < gen; i++){
            // 入左子树序列
            left.push_back(sequence[i]);
        }
        
        if(i < len-1){// 还有右子树序列时
            for(int j = i+1; j < len-i; j++){
                // 右子树序列有小与根节点的值
                if(sequence[j] < gen)
                    return false;
                // 同时入右子树序列
                right.push_back(sequence[j]);
            }
        }
        return VerifySquenceOfBST(left) && VerifySquenceOfBST(right);
    }
};

上述没通过所有测试用例,边界值情况,也就是递归出口十分混乱。

当子序列至少右两个的时候才进入下一轮递归。

class Solution {
public:
    bool VerifySquenceOfBST(vector<int> sequence) {
        int len = sequence.size();
        if(len <= 0)
           return false;
        
        int gen = sequence[len-1]; // 根节点值
        
        vector<int> left;
        vector<int> right;
        
        int i; // 左子树序列下标,都小于根节点
        for(i = 0; i < len-1, sequence[i] < gen; i++){// len-1是因为末尾是根节点
            // 入左子树序列
            left.push_back(sequence[i]);
        }
        
        if(i < len-1){// 还有右子树序列时
            for(int j = i; j < len-1; j++){
                // 右子树序列有小与根节点的值
                if(sequence[j] < gen)
                    return false;
                // 同时入右子树序列
                right.push_back(sequence[j]);
            }
        }
        
        bool bleft = true, bright = true;
        // 根节点大于1时才进入下一轮递归,因为通过上面两轮循环,子序列肯定满足条件
        if(left.size() > 1)
            bleft = VerifySquenceOfBST(left);
        if(right.size() > 1)
            bright = VerifySquenceOfBST(right);
        return bleft && bright;
    }
};
]]>
算法题
<![CDATA[【剑指Offer】287. 寻找重复数 --- 数组求重复]]>https://ariser.cn/index.php/archives/379/这篇可以作为一个类进行归纳

数组:数组求重复

1~1000放在1001长度的数组中,有一个重复值,设计算法找出。每个数组元素只能访问一次。不使用辅助空间,设计算法实现

思路1:开辟1000的哈希数组,全部置0。映射的过程中,0置一。当重复值出现的时候,发现是1,返回这个值

思路2:数学法,值全部相加sum1。1加到1000,sum2。最后sum1 - sum2得到就是多的。

思路3:异或法

  • 两相同数异或为0,否则不为0,就不同。
  • 0和n异或为n

所以 (a[1]^ a[2]^...^a[1001]) ^(1^2^...^1000),最后剩下一堆0和多余的数。结果即为这个数。

思路4:见下下题的题解

思路5:把a[i]对应的k值放到k位置,最后放进去的时候发现有重复的,这个值即为重复的。

思路6:假想一个链表,查看是否有环。环入口就是重复的元素,如下题

剑指Offer 287 - 数组重复元素1

给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。可能会出现[2,2,2,2,2]

int findDuplicate(vector<int>& nums) {
        int fast = 0;
        int slow = 0;
        
        while(1){
            fast = nums[nums[fast]];
            slow = nums[slow];
            // 找到环
            if(slow == fast){
                fast = 0;// 一个指针回到起点
                while(nums[slow] != nums[fast]){
                    fast = nums[fast];
                    slow = nums[slow];
                }
                return nums[slow];
            }
        }
    }

数组:数组中重复的元素2

给定一个整数数组 a,其中1 ≤ a[i] ≤ nn为数组长度), 其中有些元素出现两次而其他元素出现一次

找到所有出现两次的元素。不用到任何额外空间并在O(n)时间复杂度内解决这个问题

思路:凡是遇到“1 ≤ a[i] ≤ n ”,即数组值范围在下标内,可考虑二者结合起来

  • 往后遍历,访问a[i],再判断a[a[i] - 1](a[i]要先绝对值处理)

    • 大于0,a[a[i]-1]取负
    • 小于0,a[i]即为重复值。(值被变号过)
vector<int> findDuplicates(vector<int>& nums) 
{
    vector<int> twice = {};
    int temp = 0;
    for(int i = 0; i < nums.size()-1; i++){
            if(nums[i] > 0)
                temp = nums[i] - 1;
            else
                temp = (-1)*nums[i] - 1;
                
            if(nums[temp] < 0)
                twice.push_back(temp+1);
            else
                nums[temp] = -nums[temp];
            
        }
    return twice;
}
]]>
算法题
<![CDATA[【剑指Offer】T30 包含min函数的栈]]>https://ariser.cn/index.php/archives/377/

定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。

思路:

  • 借助辅助栈实现 stk_mainstk_assist
  • 入栈、出栈、取顶时,主栈都正常操作。
  • 入栈:入主栈,并判断

    • 若辅助栈空:元素入辅助栈
    • 若元素小于辅助栈顶:入辅助栈
  • 出栈:

    • 若两栈顶元素相同(证明栈顶为最小值,否则不会插入),辅助栈出栈。
    • 主栈出栈
  • 取顶:

    • 主栈stk_main.pop()
  • 取最小:直接取辅助栈最小

代码:

stack<int> stk_main, stk_assist;
void push(int value) {
    stk_main.push(value);
    
    if(stk_assist.empty() || value < stk_assist.top())
        stk_assist.push(value);
}
void pop() {
    if(stk_assist.top() == stk_assist.top())
        stk_assist.pop();
    stk_main.pop();
}
int top() {
    return stk_main.top();
}
int min() {
    return stk_assist.top();
}
]]>
算法题
<![CDATA[【剑指Offer】T32 从上往下打印二叉树(层次遍历)]]>https://ariser.cn/index.php/archives/375/

从上往下打印出二叉树的每个节点,同层节点从左至右打印。

思路:借助队列,入队根节点。打印当前层后出队,如果左右子树存在则入队。一直循环到队空。

vector<int> PrintFromTopToBottom(TreeNode* root) {
    vector<int> arr;
    if(!root)
        return arr;
    
    queue<TreeNode*> Q;
    Q.push(root);
    while(!Q.empty()){
        TreeNode* q = Q.front();
        arr.push_back(q->val);
        Q.pop();// 访问完后就出队
        
        if(q->left)
            Q.push(q->left);
        if(q->right)
            Q.push(q->right);
    }
    return arr;
}

按层打印
https://ariser.cn/index.php/archives/400/

]]>
算法题
<![CDATA[【剑指Offer】T31 栈的压入、弹出序列]]>https://ariser.cn/index.php/archives/370/

给定 pushV和 popV两个序列,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false 。

输入:pushV = [1,2,3,4,5], popV = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1

输入:pushV = [1,2,3,4,5], popV = [4,3,5,1,2]
输出:false
解释:1 不能在 2 之前弹出。

思路:

  • popupopVpushV的指针
  • 外层循环:指针停留在 popV,比较 popV[pop]和栈内元素

    • 栈空:入栈元素,pu++
    • 栈顶 != popV[pop]:入栈,pu++

      • pu == len,即pu移动到了范围外,还在本循环内,return false;
    • 栈顶 == popV[pop] :出栈、po++;

      • 数组走完,出循环,都比较完,return true;(因为退出条件是在栈状态和popV决定,绝对严格,有不符合就会退出,所以可以保证最后popV循环完,一定都匹配)
  bool IsPopOrder(vector<int> pushV,vector<int> popV) {
      int len = pushV.size();
      if(len == 0)
          return true;
      if(len == 1)
          return pushV[0] == popV[0] ? true : false;
  
      stack<int> stk;
  
      int pu = 1, po = 0; // pushV 和 popV的游标
      while(po <= len-1){
          if(stk.empty()){
              stk.push(pushV[pu]);
              pu++;
          }
  
          while(stk.top() != popV[po]){
              // 栈顶不等于带比较元素,且指针移动到了最后
              if(pu == len)
                  return false;
              // 不匹配时,入栈
              stk.push(pushV[pu]);
              pu++;
          }
          // 匹配,出栈,popV前移
          stk.pop();
          po++;
      }
      // popV一直比较到了末尾
      return true;
  }
]]>
算法题
<![CDATA[【LeetCode】109. 有序链表转换二叉搜索树 - 快慢指针对链表求中间点]]>https://ariser.cn/index.php/archives/368/

给定一个单链表,其中的元素按升序排序,将其转换为高度平衡的二叉搜索树。

本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。

给定的有序链表: [-10, -3, 0, 5, 9],
一个可能的答案是:[0, -3, 9, -10, null, 5], 它可以表示下面这个高度平衡二叉搜索树:

      0
     / \
   -3   9
   /   /
 -10  5

思路1:链表遍历完,放入数组,再对数组进行二分递归构造树,这里仅提供思路
思路2:快慢指针求链表中间节点(可以画图进行演示,很好理解),在对左右子链表递归构造二叉排序树。

TreeNode* sortedListToBST(ListNode* head) {
    if(!head)
        return nullptr;
    // 下面是快慢指针求中间节点的过程
    ListNode* slow = head;
    ListNode* fast = head;
    ListNode* pre = nullptr; // 用于对链表进行分段
    
    while(fast && fast->next){
        pre = slow;
        slow = slow->next;
        fast = fast->next->next;
    }
    // 这时slow即为中间节点,可以自己画图演示
    
    TreeNode* root = new TreeNode(slow->val);
    
    if(pre){// 确保发生了分组,最后两个节点会出错
        //断链操作 |head...slow-1(pre) | slow | slow->next....nullptr|  
        pre = nullptr;
        root->left = sortedListToBST(head);
        root->right= sortedListToBST(slow->next);
    }
    return root;
}
]]>
算法题
<![CDATA[【LeetCode】 T98. 验证二叉搜索树 递归过程有点难理解]]>https://ariser.cn/index.php/archives/366/

给定一个二叉树,判断其是否是一个有效的二叉搜索树。

假设一个二叉搜索树具有如下特征:

节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。

// 二叉排序树中序遍历有序
long last = LONG_MIN; // 父节值
bool flag = true; // 父结点是否大于子节点
bool IsBSTree(TreeNode* root){
    if(!root)
        return true;
    
    // 遍历左子树
    if(flag && root->left)
        IsBSTree(root->left);
    
    // 做判断
    // 当前节点小于等于上一个节点,不是二叉排序树
    if(root->val <= last)
        flag = false;
    last = root->data; // 记录父节点值
    
    //遍历右子树
    if(root->rchild && flag != 0)
        IsBSTree(root->right);
    
    // 树都遍历完 或 不是二叉排序树,就退出
    return flag;
}
]]>
算法题
<![CDATA[【LeetCode】T111. 二叉树的最小深度]]>https://ariser.cn/index.php/archives/364/

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量

说明: 叶子节点是指没有子节点的节点。

int minDepth(TreeNode* root) {
    if(root==NULL) 
        return 0;
    
    int LD = minDepth(root->left);
    int RD = minDepth(root->right);
    // 1+LD+RD指的是:只有一棵子树。即LD或LR有一个为0的情况,叶结点也符合
    return (LD && RD) ? 1+min(LD, RD) : 1+LD+RD;
}
]]>
算法题
<![CDATA[【LeetCode】T104.二叉树最大深度]]>https://ariser.cn/index.php/archives/360/

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数

说明: 叶子节点是指没有子节点的节点。

int maxDepth(TreeNode* root) {
    if(!root)
        return 0;
    int LD = maxDepth(root->left);
    int RD = maxDepth(root->right);
    return (LD>RD ? LD:RD) + 1;
}
]]>
算法题
<![CDATA[【LeetCode】T108 有序数组转为二叉搜索树]]>https://ariser.cn/index.php/archives/353/

将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。

思路:遇到有序数组,一般首先会想到二分法。中间值做根节点,递归构建左子树和右子树。二分递归

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* helper(vector<int>& nums, int low, int high){
        if(low > high)
            return nullptr;
        
        int mid = low + (high-low)/2;
        TreeNode* root = new TreeNode(nums[mid]); // 中间值做根节点
        
        root->left = helper(nums, low, mid-1);
        root->right = helper(nums, mid+1, high);
        return root;
    }
    
    TreeNode* sortedArrayToBST(vector<int>& nums) {
        if(nums.empty())
            return nullptr;
        
        return helper(nums, 0, nums.size()-1);
    }
};
]]>
算法题
<![CDATA[【剑指Offer】T21 调整数组顺序使奇数位于偶数前面]]>https://ariser.cn/index.php/archives/163/

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。

思路:参考快速排序

  • i++往前走碰到偶数停下来,j = i+1
  • a[j]为偶数,j++前进,直到碰到奇数

    • a[j]对应的奇数插到a[i]位置,j经过的j-i个偶数依次后移
  • 如果j==len-1时还没碰到奇数,证明ij之间都为偶数了,完成整个移动

1566376497062.png

class Solution {
public:
    void reOrderArray(vector<int> &array) {
        int len = array.size();
        if(len <= 1){ // 数组空或长度为1
            return;
        }
        
        int i = 0;
        while(i < len){
            int j = i + 1;
            if(array[i]%2 == 0){ // a[i]为偶数,j前进,直到替换
                while(array[j]%2 == 0){ // j为偶数,前进
                    j++;
                    if(j==len-1 && array[j]%2==0)// i为偶数,j也为偶数,一直后移到了末尾,证明后面都是偶数
                        return;
                }
                // 此时j为奇数
                int count = j-i;
                int temp = array[i];
                array[i] = array[j];
                while(count>1){
                    array[i+count] = array[i+count-1];//数组后移
                    count--;
                }
                array[i+1] = temp;
            }
            i++;
        }
    }
};
]]>
算法题,数据结构
<![CDATA[【剑指Offer】T18 删除链表节点]]>https://ariser.cn/index.php/archives/159/

题目一:O(1)时间内删除链表结点
给定单链表头指针和一个结点的指针,定义一个函数在O(1)时间内删除该结点。

常规思想:向后p->next,直到找到该结点,然后调整指针删除,复杂度为O(n)。这样做的原因是需要直到该节点的前一个结点。

void DeleteNode(LinkList** L, LinkList *pToBeDelete){
      LinkList *pre = L;
    while(pre->next != nullptr){
        LinkList *q = pre->next;
        // 结点在末尾的情况
        if(q ->next == nullptr){
            pre->next = nullptr;
            delete q;
            q = nullptr;
            return;
        }
        
        if(q == pToBeDelete){
            pre->next = q->next;
            delete q;
            q = nullptr;
            return;
        }
        
        pre = pre->next;
    }
}

1566283592933.png

高级思想,复杂度为1:后继结点赋值到本身,然后删除后继结点

void DeleteNode(LinkList** L, LinkList *pToBeDelete){
      
    LinkList *q = pToBeDelete->next; // 记录以删除结点
    
    pToBeDelete->data = pToBeDelete->next->data;
    pToBeDelete->next = pToBeDelete->next->next;
    // 也可用q表示
    // pToBeDelete->data = q->data;
    // pToBeDelete->next = q->next;
    delete q;
    q = nullptr;
}

1566284094823.png

加上几种特殊情况:

  • 删除结点位于尾部:遍历求解
  • 链表中只有一个结点:删除结点,并设置头结点为nullptr

完整代码:

void DeleteNode(LinkList** L, LinkList *pToBeDelete){
      // 结点在末尾的情况
    if(pToBeDelete->next == nullptr){
        LinkList *pre = *L;
        while(pre->next != pToBeDelete){
            pre = pre->next;
        }
        // 此时pre->next == pToBeDelete
        pre->next = nullptr;
        // 清理
        delete pToBeDelete;
        pToBeDelete = nullptr;
    }
    // 只有一个结点
    else if(*L == pToBeDelete){
        delete pToBeDelete;
        pToBeDelete = nullptr;
        *L = nullptr;
    } else { // 普通情况
        LinkList *q = pToBeDelete->next; // 记录以删除结点
    
        pToBeDelete->data = pToBeDelete->next->data;
        pToBeDelete->next = pToBeDelete->next->next;
        // 也可用q表示
        // pToBeDelete->data = q->data;
        // pToBeDelete->next = q->next;
        delete q;
        q = nullptr;
    }
}

T18-2 删除链表重复结点

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5

我的思路:

ListNode* deleteDuplicates(ListNode* head) {
    if(head == NULL)
        return head;

    ListNode* p = head;
    ListNode* pre = NULL; // 记录p前驱
    int flag = 0; // 记录是否发生删除操作

    while(p->next != NULL){
        pre = p;
        ListNode* pn = p->next; // p后继
        while(pn->next != NULL){ // 向后遍历
            if(pn->val == p->val){ // 后继和p相同
                pn = pn->next; //后继前进
                flag = 1;
                // 删除操作
            }else // 直到不相等,跳出遍历
                break;
        }

        if(flag == 1){ // 发生删除操作
            flag = 0;

            p->val = pn->val;

            if(pn->next == NULL){ // pn为末尾结点
                if(pn->val == p->val){ // [1, 1],最后两个结点相同的情况
                    pre->next = NULL;
                    break;
                }
                p->next = NULL;
            }
            else
                p->next = pn->next;

        }else
            p = p->next; // 无操作,前进
    }
    return head;
}

通不过测试用例[1,1][0,1,1,2,2](末尾有相同数字)

ListNode* deleteDuplication(ListNode* head){
    ListNode* p = new ListNode(0);
    p->next = head;
    head = p;
    ListNode *left,*right;
    while(p->next){
        left = p->next;
        right = left;
        while(right->next && right->next->val==left->val) // 1
            right = right->next;
        if(left == right) // 2
            p = p->next;
        else // 3 
            p->next =right->next;
    }
    return head->next;
}

1566307499061.png

]]>
算法题,数据结构
<![CDATA[【剑指Offer】T16 数值的整数次方]]>https://ariser.cn/index.php/archives/157/

给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

  1. 直接想到的方法:

    double Power(double base, int exponent) {
        double result = 1.0;
        for(int i = 1; i <= exponent; i++){
            result*=base;
        }
        return result;
    }

    缺陷:没考虑指数正负、底数为0的情况

  2. 考虑边界值

    double Power(double base, int exponent) {
        if(equal(base, 0.0)){
            return 0.0;
        }
        
        unsigned int unsignedExponent = (unsigned int)(exponent);
        if(exponent < 0){
            unsignedExponent = (unsigned int)(-exponent);
        }
        
        double result = 1.0;
        for(int i = 1; i <= unsignedExponent; i++){
            result*=base;
        }
        
        if(exponent < 0){
            return 1.0/result;
        }
        
        return result;
    }
  3. 优解:如果求32次方,知道16次方的基础上,可以直接对16次方进行平方,减少计算次数。类似递归中的斐波那契数列,有如下公式:

$$ a^n =\begin{cases} a^{n/2} \times a^{n/2},n为偶数\\ a^{(n-1)/2} \times a^{(n-1)/2},n为奇数 \end{cases} $$

double PowerWithUnsignedExponent(double base, unsigned int exponent);

double Power(double base, int exponent)
{
    g_InvalidInput = false;

    if (equal(base, 0.0) && exponent < 0)
    {
        g_InvalidInput = true;
        return 0.0;
    }

    unsigned int absExponent = (unsigned int) (exponent);
    if (exponent < 0)
        absExponent = (unsigned int) (-exponent);

    double result = PowerWithUnsignedExponent(base, absExponent);
    if (exponent < 0)
        result = 1.0 / result;

    return result;
}

double PowerWithUnsignedExponent(double base, unsigned int exponent)
{
    if (exponent == 0)
        return 1;
    if (exponent == 1)
        return base;

    double result = PowerWithUnsignedExponent(base, exponent >> 1);
    result *= result;
    if ((exponent & 0x1) == 1)
        result *= base;

    return result;
}
]]>
算法题
<![CDATA[【剑指Offer】T15 二进制中1的个数(二进制)]]>https://ariser.cn/index.php/archives/156/

输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。例如,输入9,二进制位1001,输出2
思路1:输入的n不断右移,和00...01做与运算&

  • 结果为1:最后一位为1
  • 结果为0:最后一位为0

循环停止条件:移位一直到整数变成0。

注:除2运算等同于右移一位,但是位运算效率比较高

缺点:如果输入负数,移位后最高位会变成1。一直右移会变成全1,死循环。

补充:求-3的十六进制:3的十六进制为0003,3求反之后是C,再加1(补码),成D,所以-3的十六进制就是:FFFD

思路2:把右移n变为左移000..001->000...010,再与n做与运算。

int NumberOf1(int n){
    int count = 0;
    unsigned int flag = 1;
    
    while(flag){
        if(n & flag)
            count++;
        flag = flag<<1;
    }
    return count;
}

循环次数为整数二进制的位数32。(强制循环32次结束,因为unsigned int最大长度为32)

==最优解:==一个整数减1,在和原整数与运算,会把整数最右边的1变成0。那么一个整数的二进制表示有多少个1,就可以进行这样的操作。

例如:

  • 1100 - 1 = 10111100 & 1011 = 1000
  • 1000 - 1 = 00111000 & 0011 = 0000
int Number(int n){
    int count = 0;
    while(n){
        count++;
        n = (n - 1)&n;
    }
    return count;
}

拓展:

  • 一条语句判断整数是不是2的整数次方。

    • 如果是2的整数次方,那么二进制表示有且仅有一位是1,其余都为0。所以if(n & (n - 1) == 0){}
  • 两个整数m、n,计算改变m的二进制多少位可以得到n。

    1. 求两个数的异或
    2. 统计结果中1的位数
  • 右移替代除二运算、位与替代求余运算判断技术还是偶数

    • 4 & 1 -> 0 偶数
    • 5 & 1 -> 1 奇数
]]>
算法题
<![CDATA[【剑指Offer】T14 剪绳子(动态规划、贪婪算法)]]>https://ariser.cn/index.php/archives/154/

长度为n的绳子,请把绳子剪成m段(m、n都为整数,且都大于1),每段绳子长度记为k[0],k[1],...,k[m]。请问k[0]x...xk[n]最大乘积是多少?例如,长度为8时,剪为长度为2、3、3三段,最大乘积为18。

思路:f(2)f(3)可以先求得,最优为1、1和1、2。再把递归问题化为剩下的迭代

int maxProductAfterCutting_solution(int length){
    if(length < 2)
        return 0;
    if(length == 2)
        return 1;
    if(length == 3)
        return 2;
    
    int *products = new int[length+1]; // 每个长度下对应最大值(最优长度)
    products[0] = 0;
    products[1] = 1;
    products[2] = 2;
    products[3] = 3;
    
    int max = 0;
    
    for(int i = 4; i <= length; i++){ // 从长度4开始进行讨论,记录最大值
        max = 0;
        
        for(int j = 0; j <= i/2; j++){ // 对当前长度迭代所有情况,记录
            int product = products[j] * products[i-j]; // 裁剪长度乘以剩下长度
            if(max < product){
                max = product;
            }
        }
        products[i] = max;
    }
    max = products[length];
    delete[]products;
    return max;
}

贪婪算法的思路:

  • 长度大于5时,尽可能多剪长度为3的绳子
  • 剩下长度为4时,绳子剪为两个长度为2的绳子
int maxProductAfterCutting_solution(int length){
    if(length < 2)
        return 0;
    if(length == 2)
        return 1;
    if(length == 3)
        return 2;

    int timesOf3 = length / 3;
    if(length - timesOf3*3 == 1){
        timesOf3-=1;
    }
    
    int timesOf2 = (length - timesOf3*3)/2;
    
    return (int)(pow(3, timesOf3) * (int)(pow(2, timesOf2)));
}

1566223041921.png

]]>
算法题
<![CDATA[【剑指Offer】T11 旋转数组最小的数字]]>https://ariser.cn/index.php/archives/151/

题目描述:
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

分析:二分查找变种,没有具体的值用来比较。那么用中间值和高低位进行比较,看处于递增还是递减序列。

  • 处于递增:low上移
  • 处于递减:high下移(如果是high-1,则可能会错过最小值,因为找的就是最小值)
  • 其余情况:low++缩小范围
    1566215529164.png
  • 特殊情况:
    1566216946200.png
int minNumberInRotateArray(vector<int> rotateArray) {
        if(rotateArray.empty())
            return 0;
        
        int low = 0;
        int high = rotateArray.size() - 1;
        int mid = 0;
        
        while(low < high){
            // 子数组是非递减的数组,7890112
            if (rotateArray[low] < rotateArray[high]) 
                return rotateArray[low];
            mid = low + (high - low) / 2;
            if(rotateArray[mid] > rotateArray[low])
                low = mid + 1;
            else if(rotateArray[mid] < rotateArray[high])
                high = mid;
            else low++;
        }
        return rotateArray[low];
    }

边界值搞死人,如果没有书中的分析以及输出的未通过的测试用例,那么将很难想到特殊情况。

]]>
算法题
<![CDATA[【剑指Offer】T10 斐波那契数列及其变种]]>https://ariser.cn/index.php/archives/149/

斐波那契数列

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39
循环实现:

long long Fibonacci(unsigned int n){
//    if(n == 0)
//        return 0;
//    if(n == 1)
//        return 1;
//    return Fibonacci(n-1) + Fibonacci(n - 2);

    int result[2] = {0, 1};
    if(n < 2){
        return result[n];
    }

    long long  fbn_1 = 0;
    long long  fbn_2 = 1;
    long long  fbn = 0;
    for(int i = 2; i <= n; i++){
        fbn = fbn_1 + fbn_2;
        fbn_1 = fbn_2;
        fbn_2 = fbn;
    }
    return fbn;

}

递归实现:

long long Fibonacci(unsigned int n){
    if(n == 0)
        return 0;
    if(n == 1)
        return 1;
    return Fibonacci(n-1) + Fibonacci(n - 2);
}
    

青蛙跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

递归实现:

int jumpFloor(int number) {
        if(number < 2)
            return number;
 
        if(n >= 2){
            return FrogJump(n-1) + FrogJump(n-2);
        }
    }

非递归实现:

int jumpFloor(int number) {
        if(number < 2)
            return number;
 
        int fbn1 = 0;
        int fbn2 = 1;
        int fbn = 0;
 
        for(int i = 1; i <= number; i++){
            fbn = fbn1 + fbn2;
            fbn1 =fbn2;
            fbn2 = fbn;
        }
        return fbn;
}

题目10-3 变态青蛙跳台阶---没做

方块填充

我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

class Solution {
public:
    int rectCover(int number) {
        if(number <= 2){
            return number;
        }
         
        int method1 = 1;
        int method2 = 2;
        int method = 0;
         
        for(int i = 3; i <= number; i++){
            method = method1 + method2;
            method1 = method2;
            method2 = method;
        }
        return method;
    }
};

也是斐波那契数列,这种题如果要用迭代法,从底层开始分析,则需要更改迭代起始位置的,这里是从3开始

]]>
默认分类
<![CDATA[【剑指Offer】T9 两个栈实现队列]]>https://ariser.cn/index.php/archives/148/

用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型

  • 出栈:

    1. 栈2空:(栈一元素到栈二,再栈二出栈)

      • 取栈一的顶
      • push到栈2
      • pop栈一
      • 直到栈一空
      • 栈二出栈
    2. 栈2不空:栈二出栈
  • 入栈:栈一push
class Solution
{
public:
    void push(int node) {
        stack1.push(node);
    }

    int pop() {
        if(stack2.empty()){
            while(!stack1.empty()){
                stack2.push(stack1.top());
                stack1.pop();
            }
            
        }
        int temp = stack2.top();
        stack2.pop();
        return temp;
    }

private:
    stack<int> stack1;
    stack<int> stack2;
};

扩展:两个队列实现栈
https://www.jianshu.com/p/285419bfa880

出栈:

//如果 queueA 的大小不为 0 则循环取出元素
while(queueA.size() > 0){
    //被取出的元素
    int result = queueA.poll();
    // 这里注意我们取出元素后再去判断一次,队列是否为空,如果为空代表是最后一个元素
    if(queueA.size() != 0){
        queueB.add(result)
    }else{
        return result;
    }
}
  • 任何时候两个队列总有一个是空的。
  • 添加元素总是向非空队列中 add 元素。
  • 取出元素的时候总是将元素除队尾最后一个元素外,导入另一空队列中,最后一个元素出队。
]]>
算法题
<![CDATA[【剑指Offer】T7 重建二叉树]]>https://ariser.cn/index.php/archives/144/

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

思路参考《王道数据结构》P120

思路:

  1. 由先序序列第一个pre[0]在中序序列中找到根节点位置gen
  2. gen为中心遍历

    • 0~gen左子树

      • 子中序序列:0~gen-1,放入vin_left[]
      • 子先序序列:1~gen放入pre_left[]+1可以看图,因为头部有根节点
    • gen+1~vinlen为右子树

      • 子中序序列:gen+1 ~ vinlen-1放入vin_right[]
      • 子先序序列:gen+1 ~ vinlen-1放入pre_right[]
  3. 由先序序列pre[0]创建根节点
  4. 连接左子树,按照左子树子序列递归(pre_left[]vin_left[]
  5. 连接右子树,按照右子树子序列递归(pre_right[]vin_right[]
  6. 返回根节点

1566200739629.png
1566200965090.png

TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
    int vinlen=vin.size();

    if(vinlen==0)
        return NULL;

    vector<int> pre_left, pre_right, vin_left, vin_right;

    //创建根节点,根节点肯定是前序遍历的第一个数
    TreeNode* head = new TreeNode(pre[0]);

    //找到中序遍历根节点所在位置,存放于变量gen中
    int gen=0;
    for(int i=0;i<vinlen;i++){
        if(vin[i]==pre[0]){
            gen=i;
            break;
        }
    }

    //对于中序遍历,根节点左边的节点位于二叉树的左边,根节点右边的节点位于二叉树的右边
    // 左子树
    for(int i = 0; i < gen; i++){
        vin_left.push_back(vin[i]);
        pre_left.push_back(pre[i+1]);//先序第一个为根节点
    }
    // 右子树
    for(int i = gen + 1; i < vinlen; i++){
        vin_right.push_back(vin[i]);
        pre_right.push_back(pre[i]);
    }
    //递归,执行上述步骤,区分子树的左、右子子树,直到叶节点
    head->left = reConstructBinaryTree(pre_left, vin_left);
    head->right = reConstructBinaryTree(pre_right, vin_right);
    return head;

}

leetcode T106 中序和后续构建二叉树

TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
    int len = postorder.size();
    if(len == 0)
        return NULL;

    // 在中序序列中找到根节点位置
    int gen = 0;
    for(gen = 0; gen < len, inorder[gen] != postorder[len-1]; gen++);

    vector<int> in_left, in_right, pos_left, pos_right;

    for(int i = 0; i < gen; i++){
        in_left.push_back(inorder[i]);
        pos_left.push_back(postorder[i]);
    }

    for(int i = gen+1; i < len; i++){
        in_right.push_back(inorder[i]);
        pos_right.push_back(postorder[i-1]);
    }

    TreeNode* head = new TreeNode(postorder[len-1]);
    head->left = buildTree(in_left, pos_left);
    head->right = buildTree(in_right, pos_right);

    return head;
}
]]>
算法题,数据结构