书接上篇

81 阅读24分钟

数据结构

1、数据结构的存储一般常用的有几种?各有什么特点?

顺序存储结构 :
数据元素顺序存放,每个结点只有一个元素。存储位置反映数据元素间的逻辑关系。
    存储密度大,但是插入、删除操作效率较差。(比如:数组:1-2-3-4-5-6-7-8-9-10,存储是按顺序的。再比如栈和队列等)。
链式存储结构 :
每个结点除了包含数据元素信息外还包含一组指针,指针反映数据元素间的逻辑关系。
    这种存储方式不要求存储空间连续,便于进行插入和删除操作,但是存储空间利用率较低。
    另外,由于逻辑上相邻的数据元素在存储空间上不一定相邻,所以不能对其进行随机存取。
哈希(散列)存储结构 :
通过哈希函数解决冲突的方法,将关键字散列在连续的 有限的地址空间内,并将哈希函数的值作为该数据元素的存储地址。
    其特点是存取速度快,只能按关键字随机存取,不能顺序窜出,也不能折半存取。
索引存储结构 :
索引存储除了数据元素存储在一地址连续的内存空间外,尚需建立一个索引表。索引表中的索引指示结点的存储位置,兼有动态和静态的特性。

2、集合结构 线性结构 树形结构 图形结构

集合结构:就是一个集合,就是一个圆圈中有很多个元素,元素与元素之间没有任何关系 。
线性结构 :就是一个条线上站着很多个人。 这条线不一定是直的。也可以是弯的。也可以是直的,相当于一条线被分成了好几段的样子。 线性结构是一对一的关系。
树形结构 :做开发的肯定或多或少的知道xml 解析 。树形结构跟他非常类似。也可以想象成一个金字塔。树形结构是一对多的关系。
图形结构:这个就比较复杂了。 无穷、无边、 无向(没有方向)图形机构。你可以理解为多对多,类似于我们人的交集关系。

3、链表、单向链表、双向链表、循环链表

链表:
是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:
    一个是存储数据元素的数据域。
    另一个是存储下一个结点地址的指针域。

相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
.

单向链表:
A->B->C->D->E->F->G->H。 这就是单向链表 ,H 是头 A 是尾,像一个只有一个头的火车一样。只能一个头拉着跑。

双向链表:
H<- A->B->C->D->E->F->G->H。 这就是双向链表。有头没尾,两边都可以跑 ,跟地铁一样 到头了,可以倒着开回来。

循环链表:
A->B->C->D->E->F->G->H->A,绕成一个圈。就像蛇吃自己的这就是循环。

4、数组和链表区别

数组是可以在内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从0开始。
优点:
    按照索引查询元素速度快
    按照索引遍历数组方便

缺点:
    数组的大小固定后就无法扩容了
    数组只能存储一种类型的数据
    添加,删除的操作慢,因为要移动其他的元素。

适用场景:
频繁查询,对存储空间要求不大,很少增加和删除的情况。

链表:
是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:
    一个是存储数据元素的数据域。
    另一个是存储下一个结点地址的指针域。

相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

5、堆、栈和队列

堆:
    堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即 动态分配内存,对其访问和对一般内存的访问没有区别。堆是指程序运行时申请的动态内存,而栈只是指一种使用堆的方法(即先进后出)。
    是一种比较特殊的数据结构,可以被看做一棵树的数组对象,具有以下的性质:
    堆中某个节点的值总是不大于或不小于其父节点的值;
    堆总是一棵完全二叉树。
    堆分为两种情况,有最大堆和最小堆。
    将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆,在一个摆放好元素的最小堆中,父结点中的元素一定比子结点的元素要小,但对于左右结点的大小则没有规定谁大谁小。
    堆常用来实现优先队列,堆的存取是随意的,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。

栈:
    栈是一种具有先进后出的数据结构,又称为先进后出的线性表,简称 FILO(—First-In/Last-Out)结构。也就是说后存放的先取,先存放的后取,这就类似于我们要在取放在箱子底部的东西(放进去比较早的物体),我们首先要移开压在它上面的物体(放进去比较晚的物体)。
    栈是限定仅在表尾进行插入和删除操作的线性表。我们把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈的特殊之处在于它限制了这个线性表的插入和删除位置,它始终只在栈顶进行。
    堆栈中定义了一些操作。两个最重要的是PUSH和POP。PUSH操作在堆栈的顶部加入一个元素。POP操作相反,在堆栈顶部移去一个元素,并将堆栈的大小减一。
    系统会给栈自动分配内存空间。
    常用:
    递归(如:逆序输出)
    语法检查,符号成对出现
    数制转换
    二叉树的一些操作

队列:
    队列是一种先进先出(FIFO—first in first out)的数据结构,又称为先进先出的线性表,简称 FIFO(First In First Out)结构。也就是说先放的先取,后放的后取,就如同行李过安检的时候,先放进去的行李在另一端总是先出来,后放入的行李会在最后面出来。
    队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。允许插入的一端称为队尾,允许删除的一端称为队头。它是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作(队头做删除,队尾做插入)。和栈一样,队列是一种操作受限制的线性表。
    在这里插入图片描述

6、二叉树相关操作 7、输入一棵二叉树的根结点,求该树的深度?

如果一棵树只有一个结点,它的深度为1。
如果根结点只有左子树而没有右子树, 那么树的深度应该是其左子树的深度加1。
同样如果根结点只有右子树而没有左子树,那么树的深度应该是其右子树的深度加1。
如果既有右子树又有左子树, 那该树的深度就是其左、右子树深度的较大值再加1。

public static int treeDepth(BinaryTreeNode root) {
    if (root == null) {
        return 0;
    }
    int left = treeDepth(root.left);
    int right = treeDepth(root.right);
    return left > right ? (left + 1) : (right + 1);
}

8、输入一课二叉树的根结点,判断该树是不是平衡二叉树?

重复遍历结点

先求出根结点的左右子树的深度;
然后判断它们的深度相差不超过1,如果否,则不是一棵二叉树;
如果是,再用同样的方法分别判断左子树和右子树是否为平衡二叉树,如果都是,则这就是一棵平衡二叉树。

遍历一遍结点
遍历结点的同时记录下该结点的深度,避免重复访问。
方法:

bool IsBalanced_1(TreeNode* pRoot,int& depth){ if(pRoot==NULL){ depth=0; return true; } int left,right; int diff; if(IsBalanced_1(pRoot->left,left) && IsBalanced_1(pRoot->right,right)){ diff=left-right; if(diff<=1 || diff>=-1){ depth=left>right?left+1:right+1; return true; } } return false; }

bool IsBalancedTree(TreeNode* pRoot){ int depth=0; return IsBalanced_1(pRoot,depth); }

算法 1、时间复杂度

在计算机科学中,时间复杂性,又称时间复杂度。 算法的时间复杂度是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。 2、空间复杂度

空间复杂度(Space Complexity)是对一个算法在运行过程中 临时占用存储空间大小的量度,记做S(n)=O(f(n))。比如: 直接插入排序的时间复杂度是O(n^2),空间复杂度是O(1) 。而一般的递归算法就要有O(n)的空间复杂度了,因为每次递归都要存储返回信息。 一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量:时间复杂度 & 空间复杂度 。 3、常用的排序算法

冒泡排序:
    原理:就是重复地走访过要排序的元素列,依次比较两个相邻的元素,顺序不对就交换,直至没有相邻元素需要交换,也就是排序完成。
    这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
    冒泡排序是一种稳定排序算法。
    时间复杂度:最好情况(初始情况就是正序)下是o(n),平均情况是o(n²)

/** 
 *	【冒泡排序】:相邻元素两两比较,比较完一趟,最值出现在末尾
 *	第1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n个元素位置
 *	第2趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n-1个元素位置
 *	 ……   ……
 *	第n-1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第2个元素位置	
 */
void bublleSort(int *arr, int length) {
    for(int i = 0; i < length - 1; i++) { //趟数
        for(int j = 0; j < length - i - 1; j++) { //比较次数
            if(arr[j] > arr[j+1]) {
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        } 
    }
}

选择排序
    选择排序(Selection sort)是一种简单直观的排序算法。原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。
    选择排序是不稳定的排序方法 。
    时间复杂度:最好和平均情况下都是O(n²)

/** 
 *	【选择排序】:最值出现在起始端
 *	
 *	第1趟:在n个数中找到最小(大)数与第一个数交换位置
 *	第2趟:在剩下n-1个数中找到最小(大)数与第二个数交换位置
 *	重复这样的操作...依次与第三个、第四个...数交换位置
 *	第n-1趟,最终可实现数据的升序(降序)排列。
 *
 */
void selectSort(int *arr, int length) {
    for (int i = 0; i < length - 1; i++) { //趟数
        for (int j = i + 1; j < length; j++) { //比较次数
            if (arr[i] > arr[j]) {
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
    }
}

直接插入排序
    插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,
    插入排序的基本思想是:每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止
    直接插入排序是稳定的排序算法。
    时间复杂度:最好情况(初始情况就是正序)下是o(n),平均情况是o(n²)

/**
 *
 *	num[] 是已经排序好的,在插入一个数直接进行排序
 *
 */
void insertSort2(int num[],int count)  {
    int i,j;
    for (i = 1; i < count; i++) {
        if (num[i] < num[i - 1]) { // 当前数比前一位的数小
            int temp = num[i]; // 记住 当前数
            for (j = i; j > 0; j--) { // 从当前数起 逆序
                if (num[j - 1] > temp) num[j] = num[j - 1]; // 如果 当前数比前一位小,前一位后移
                else break;
            }
            num[j] = temp; 
        }
    }
}

二分插入排序
    由于在插入排序过程中,待插入数据左边的序列总是有序的,针对有序序列,就可以用二分法去插入数据了,也就是二分插入排序法。适用于数据量比较大的情况。
    二分插入排序的算法思想:
    算法的基本过程:
    (1)计算 0 ~ i-1 的中间点,用 i 索引处的元素与中间值进行比较,如果 i 索引处的元素大,说明要插入的这个元素应该在中间值和刚加入i索引之间,反之,就是在刚开始的位置到中间值的位置,这样很简单的完成了折半**;
    (2)在相应的半个范围里面找插入的位置时,不断的用(1)步骤缩小范围,不停的折半,范围依次缩小为 1/2 1/4 1/8 …快速的确定出第 i 个元素要插在什么地方;
    (3)确定位置之后,将整个序列后移,并将元素插入到相应位置。
    二分插入排序是稳定的排序算法。
    时间复杂度:最好情况(刚好插入位置为二分位置)下是O(log₂n),平均情况和最坏情况是o(n²)

/**
 *	折半查找:优化查找时间(不用遍历全部数据)
 *
 *	折半查找的原理:
 *   1> 数组必须是有序的
 *   2> 必须已知min和max(知道范围)
 *   3> 动态计算mid的值,取出mid对应的值进行比较
 *   4> 如果mid对应的值大于要查找的值,那么max要变小为mid-1
 *   5> 如果mid对应的值小于要查找的值,那么min要变大为mid+1
 *
 */ 
 
// 已知一个有序数组, 和一个key, 要求从数组中找到key对应的索引位置 
int findKey(int *arr, int length, int key) {
    int min = 0, max = length - 1, mid;
    while (min <= max) {
        mid = (min + max) / 2; //计算中间值
        if (key > arr[mid]) {
            min = mid + 1;
        } else if (key < arr[mid]) {
            max = mid - 1;
        } else {
            return mid;
        }
    }
    return -1;
}

希尔排序
    希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。
    希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,排序完成。
    希尔排序是不稳定排序算法。
    时间复杂度:O(n^(1.3—2))

void shellSort(int num[],int count)
{
    int shellNum = 2;
    int gap = round(count/shellNum);

    while (gap > 0) {
        for (int i = gap; i < count; i++) {
            int temp = num[i];
            int j = i;
            while (j >= gap && num[j - gap] > temp) {
                num[j] = num[j - gap];
                j = j - gap;
            }
            num[j] = temp;
        }
        gap = round(gap/shellNum);
    }
}


快速排序
    快速排序(Quicksort)是对冒泡排序的一种改进。
    它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
    快速排序是不稳定的排序算法。
    时间复杂度:最差为O(n^2),平均为O(nlogn),最好为O(nlogn)。

void quickSort(int num[], int left, int right)
{
    if (left >= right){ // 如果left >= right说明排序结束了
        return ;
    }
    // 变量key为基准数,在此规定基准数为序列的第一个数,即左指针指向的数
    int key = num[left];
    int i  = left;           //左指针
    int j  = right;          //右指针
    int temp;
    
    while (i != j) { // 该 while 循环结束一次表示比较了一轮
 		while(i < j && arr[j] >= key) { // 从右向左找第一个小于key的数
             j--;
 		}
 		while(i < j && arr[i] < key) { // 从左向右找第一个大于等于key的数
             i++;
 		}
 		
 		if(i < j) {
 			temp = arr[i];
 			arr[i] = arr[j];
         	arr[j] = temp;
        }
 	}
 	
 	arr[left] = arr[i];
 	arr[i] = key;
 	
    // 分治方法进行递归
    quickSort(num, left, i - 1);
    quickSort(num, i + 1, right);
}


堆排序
    是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点
    在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
        最大堆调整(Max Heapify): 将堆的末端子节点作调整,使得子节点永远小于父节点
        创建最大堆(Build Max Heap): 将堆中的所有数据重新排序
        堆排序(HeapSort): 移除位在第一个数据的根节点,并做最大堆调整的递归运算
    堆排序是一个非稳定的排序算法。
    时间复杂度:O(nlogn)

void maxHeapify(int num[], int start, int end) {
    //建立父节点指标和子节点指标
    int dad = start;
    int son = dad * 2 + 1;
    while (son <= end) { //若子节点指标在范围内才做比较
        if (son + 1 <= end && num[son] < num[son + 1]) //先比较两个子节点大小,选择最大的
            son++;
        if (num[dad] > num[son]) //如果父节点大於子节点代表调整完毕,直接跳出函数
            return;
        else { //否则交换父子内容再继续子节点和孙节点比较
            EXCHANGE(num[dad], num[son])
            dad = son;
            son = dad * 2 + 1;
        }
    }
}

void heapSort(int num[], int count) {
    int i;
    //初始化,i从最後一个父节点开始调整
    for (i = count / 2 - 1; i >= 0; i--)
        maxHeapify(num, i, count - 1);
    //先将第一个元素和已排好元素前一位做交换,再重新调整,直到排序完毕
    for (i = count - 1; i > 0; i--) {
        EXCHANGE(num[0], num[i])
        maxHeapify(num, 0, i - 1);
    }
}

4、字符串反转

  • (NSString *)reversalString:(NSString *)originString{ NSString *resultStr = @""; for (NSInteger i = originString.length -1; i >= 0; i--) { NSString *indexStr = [originString substringWithRange:NSMakeRange(i, 1)]; resultStr = [resultStr stringByAppendingString:indexStr]; } return resultStr; }

5、链表反转(头差法)

头插法 :
将链表每个节点依次取下来头插到新链表,即为原链表的反转;因为改变了当前节点的 next 指向,必须先保存 next 地址。

struct ListNode* reverseList(struct ListNode* head){
    //新链表的头指针
    struct ListNode* newhead = NULL;
    //需要头插的结点
    struct ListNode* cur = head;
    
    while(cur)
    {
        //保存需要头插结点的下一个节点
        struct ListNode* next = cur->next;
        //将cur头插到新链表
        cur->next = newhead;
        newhead = cur;
        cur = next;
    }
    return newhead;
}

迭代法:
在遍历列表时,将当前节点的 next 指针改为指向前一个元素。由于节点没有引用其上一个节点,因此必须事先存储其前一个元素。在更改引用之前,还需要另一个指针来存储下一个节点。不要忘记在最后返回新的头引用。

struct ListNode* reverseList(struct ListNode* head){
    struct ListNode* pre = NULL;
    //需要反转指向的结点
    struct ListNode* cur = head;
    
    while(cur)
    {
        //保存需要头插结点的下一个节点
        struct ListNode* next = cur->next;
        //将cur头插到新链表
        cur->next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
}

6、有序数组合并

  • (void)merge { /* 有序数组A:1、4、5、8、10...1000000,有序数组B:2、3、6、7、9...999998,A、B两个数组不相互重复,请合并成一个有序数组C,写出代码和时间复杂度。 */ //(1). NSMutableArray *A = [NSMutableArray arrayWithObjects:@4,@5,@8,@10,@15, nil]; NSMutableArray *B = [NSMutableArray arrayWithObjects:@2,@6,@7,@9,@11,@12,@13, nil]; NSMutableArray *C = [NSMutableArray array]; int count = (int)A.count+(int)B.count; int index = 0; for (int i = 0; i < count; i++) { if (A[0]<B[0]) { [C addObject:A[0]]; [A removeObject:A[0]]; } else if (B[0]<A[0]) { [C addObject:B[0]]; [B removeObject:B[0]]; } if (A.count==0) { [C addObjectsFromArray:B]; NSLog(@"C = %@",C); index = i+1; NSLog(@"index = %d",index); return; } else if (B.count==0) { [C addObjectsFromArray:A]; NSLog(@"C = %@",C); index = i+1; NSLog(@"index = %d",index); return; } } //(2). //时间复杂度 //T(n) = O(f(n)):用"T(n)"表示,"O"为数学符号,f(n)为同数量级,一般是算法中频度最大的语句频度。 //时间复杂度:T(n) = O(index); }

7、查找第一个只出现一次的字符(Hash查找)

两个思路:

hash 不同编译器对字符数据的处理不一样,所以hash之前先把字符类型转成无符号类型;
空间换时间,用buffer数组记录当前只找到一次的字符,避免二次遍历。

define SIZE 256

char GetChar(char str[]) { if(!str) return 0; char* p = NULL; unsigned count[SIZE] = {0}; char buffer[SIZE]; char* q = buffer; for(p=str; *p!=0; p++) { if(++count[(unsigned char)*p] == 1) *q++ = *p; }

for (p=buffer; p<q; p++) { if(count[(unsigned char)*p] == 1) return *p; } return 0; }

8、查找两个子视图的共同父视图

这个问的其实是数据结构中的二叉树,查找一个普通二叉树中两个节点最近的公共祖先问题。 假设两个视图为UIViewA、UIViewC,其中 UIViewA继承于UIViewB,UIViewB继承于UIViewD,UIViewC也继承于UIViewD;即 A->B->D,C->D

方法1:

- (void)viewDidLoad {
    [super viewDidLoad];
    Class commonClass1 = [self commonClass1:[ViewA class] andClass:[ViewC class]];
    NSLog(@"%@",commonClass1);
    // 输出:2018-03-22 17:36:01.868966+0800 两个UIView的最近公共父类[84288:2458900] ViewD
}
// 获取所有父类
- (NSArray *)superClasses:(Class)class {
    if (class == nil) {
        return @[];
    }
    NSMutableArray *result = [NSMutableArray array];
    while (class != nil) {
        [result addObject:class];
        class = [class superclass];
    }
    return [result copy];
}
 
- (Class)commonClass1:(Class)classA andClass:(Class)classB {
    NSArray *arr1 = [self superClasses:classA];
    NSArray *arr2 = [self superClasses:classB];
    for (NSUInteger i = 0; i < arr1.count; ++i) {
        Class targetClass = arr1[i];
        for (NSUInteger j = 0; j < arr2.count; ++j) {
            if (targetClass == arr2[j]) {
                return targetClass;
            }
        }
    }
    return nil;
}

方法2:
方法一明显的是两层for循环,时间复杂度为 O(N^2) 一个改进的办法:我们将一个路径中的所有点先放进NSSet中.因为NSSet的内部实现是一个hash表,所以查询元素的时间的复杂度变成 O(1),我们一共有N个节点,所以总时间复杂度优化到了O(N)

- (Class)commonClass2:(Class)classA andClass:(Class)classB{
    NSArray *arr1 = [self superClasses:classA];
    NSArray *arr2 = [self superClasses:classB];
    NSSet *set = [NSSet setWithArray:arr2];
    for (NSUInteger i =0; i<arr1.count; ++i) {
        Class targetClass = arr1[i];
        if ([set containsObject:targetClass]) {
            return targetClass;
        }
    }
    return nil;
}

9、无序数组中的中位数(快排思想)

//求一个无序数组的中位数 int findMedian(int a[], int aLen) { int low = 0; int high = aLen - 1;

int mid = (aLen - 1) / 2;
int div = PartSort(a, low, high);

while (div != mid) {

    if (mid < div)  {
        //左半区间找
        div = PartSort(a, low, div - 1);
    }
    else  {
        //右半区间找
        div = PartSort(a, div + 1, high);
    }
}
//找到了
return a[mid];

}

int PartSort(int a[], int start, int end) { int low = start; int high = end;

//选取关键字
int key = a[end];

while (low < high) {

    //左边找比key大的值
    while (low < high && a[low] <= key)  {
        ++low;
    }
    
    //右边找比key小的值
    while (low < high && a[high] >= key) {
        --high;
    }
    
    if (low < high) {
        //找到之后交换左右的值
        int temp = a[low];
        a[low] = a[high];
        a[high] = temp;
    }
}

int temp = a[high];
a[high] = a[end];
a[end] = temp;

return low;

}

10、给定一个整数数组和一个目标值,找出数组中和为目标值的两个数

假设每个输入只对应一种答案,且同样的元素不能被重复利用。 示例:给定nums = [2, 7, 11, 15], target = 9 — 返回 [0, 1] 思路:

第一层for循环从索引0到倒数第二个索引拿到每个数组元素,
第二个for循环遍历上一层for循环拿到的元素的后面的所有元素。

class Solution { public int[] twoSum(int[] nums, int target) { int len = nums.length; int[] result = new int[2]; for(int i = 0; i < len; i++){ for(int j = i+1; j < len; j++){ if(nums[i] + nums[j] == target){ result[0] = i; result[1] = j; return result; } } } return result; } }

网络

1、谈谈对 HTTP、HTTPS 的理解

HTTP协议:超文本传输协议,他是基于TCP应用层协议
    是无连接 无状态 的,需要通过cookies 或者 session 来保持会话
    HTTP 分为两部分:请求报文和响应报文
        请求报文四个部分组成:请求行、请求头、空行、请求体
        请求报文四个部分组成:状态行、响应头、空行、响应体
        HTTP 组成

客户端请求:
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
服务端响应:

HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain
输出结果:

Hello World! My payload includes a trailing CRLF.


    URL 构成:

    preview

    协议构成:

    请求行、请求头、请求体

    常用的请求方式?

    答: GET、POST、PUT、DELETE、HEAD、OPTIONS?

    GET 和 POST 的区别?
        GET 把参数通过 ? 和 & 拼接在URL 后面,POST 放在 body 里面
        GET 有长度限制(一般2048字符),POST没有限制
        由于参数的存放,POST 相对于 GET 安全,相对安全是因为 POST 仍可以被抓包
        GET 是可以被缓存的,POST 不可被缓存(后台使用Redis、Memcached 登记室除外)

HTTPS 协议:
HTTPS是一种通过计算机网络进行安全通信的传输协议(以安全为目标),经由HTTP进行通信,利用SSL/TLS建立全信道,加密数据包。HTTPS使用的主要目的是提供对网站服务器的身份认证,同时保护交换数据的隐私与完整性(传输加密和身份认证保证了传输过程的安全性)。
PS: TLS是传输层加密协议,前身是SSL协议。HTTPS 的安全基础是 SSL 。
通过抓包可以看到数据不是明文传输,而且HTTPS有如下特点:
    HTTPS特点:
    基于HTTP协议,通过SSL或TLS提供加密处理数据、验证对方身份以及数据完整性保护
    这里写图片描述
    通过抓包可以看到数据不是明文传输,而且HTTPS有如下特点:
    内容加密:采用混合加密技术,中间者无法直接查看明文内容
    验证身份:通过证书认证客户端访问的是自己的服务器
    保护数据完整性:防止传输的内容被中间人冒充或者篡改
    混合加密:结合非对称加密和对称加密技术。客户端使用对称加密生成密钥对传输数据进行加密,然后使用非对称加密的公钥再对秘钥进行加密,所以网络上传输的数据是被秘钥加密的密文和用公钥加密后的秘密秘钥,因此即使被黑客截取,由于没有私钥,无法获取到加密明文的秘钥,便无法获取到明文数据。
    数字摘要:通过单向hash函数对原文进行哈希,将需加密的明文“摘要”成一串固定长度(如128bit)的密文,不同的明文摘要成的密文其结果总是不相同,同样的明文其摘要必定一致,并且即使知道了摘要也不能反推出明文。
    数字签名技术:数字签名建立在公钥加密体制基础上,是公钥加密技术的另一类应用。它把公钥加密技术和数字摘要结合起来,形成了实用的数字签名技术。
    收方能够证实发送方的真实身份;
    发送方事后不能否认所发送过的报文;
    收方或非法者不能伪造、篡改报文。
    在这里插入图片描述
    非对称加密过程需要用到公钥进行加密,那么公钥从何而来?其实公钥就被包含在数字证书中,数字证书通常来说是由受信任的数字证书颁发机构CA,在验证服务器身份后颁发,证书中包含了一个密钥对(公钥和私钥)和所有者识别信息。数字证书被放到服务端,具有服务器身份验证和数据传输加密功能。

    HTTPS 的验证流程?
    归纳为5个步骤:
        客户端发起一个http请求,告诉服务器自己支持哪些hash算法。
        服务端把自己的信息以数字证书的形式返回给客户端(证书内容有密钥公钥,网站地址,证书颁发机构,失效日期等)。证书中有一个公钥来加密信息,私钥由服务器持有。
        验证证书的合法性:
        客户端收到服务器的响应后会先验证证书的合法性(证书中包含的地址与正在访问的地址是否一致,证书是否过期)。
        生成随机密码(RSA签名):
        如果验证通过,或用户接受了不受信任的证书,客户端就会生成一个随机的对称密钥(session key)并用公钥加密,让服务端用私钥解密,解密后就用这个对称密钥进行传输了,并且能够说明服务端确实是私钥的持有者。
        生成对称加密算法:
        验证完服务端身份后,客户端生成一个对称加密的算法和对应密钥,以公钥加密之后发送给服务端。此时被黑客截获也没用,因为只有服务端的私钥才可以对其进行解密。之后客户端与服务端可以用这个对称加密算法来加密和解密通信内容了。

    数字证书都有哪些内容?
        Issuer – 证书的发布机构
        发布证书的机构,指明证书是哪个公司创建的(并不是指使用证书的公司)。出了问题具体的颁发机构是要负责的。
        Valid from,Valid to – 证书的有效期
        证书的使用期限。过了这个期限证书就会作废,不能使用。
        Public key – 公钥
        通常是一个字符串或数字,进行加密/解密算法时使用。公钥和私钥都是密钥,只不过一般公钥是对外开放的,加密时使用;私钥是不公开的,解密时使用。
        Subject – 主题
        证书是颁发给谁了,一般是个人或公司名称或机构名称或公司网站的网址。
        Signature algorithm- – 签名所使用的算法
        数字证书的数字签名所使用的加密算法,根据这个算法可以对指纹解密。指纹加密的结果就是数字签名。
        Thumbprint,Thumbprint algorithm – 指纹以及指纹算法(一种HASH算法)
        指纹和指纹算法会使用证书机构的私钥加密后和证书放在一起。
        主要用来保证证书的完整性,确保证书没有修改过。
        使用者在打开证书时根据指纹算法计算证书的hash值,和刚开始的值一样,则表示没有被修改过。

    客户端如何检测数字证书是合法的并是所要请求的公司的?
        首先应用程序读取证书中的 Issuer(发布机构),然后会在操作系统或浏览器内置的 受信任的发布机构中去找该机构的证书(为什么操作系统会有受信任机构的证书?先看完这个流程再来回答)。
        如果找不到就说明证书是水货,证书有问题,程序给错误信息。
        如果找到了,或用户确认使用该证书。就会拿上级证书的公钥,解密本级证书,得到数字指纹。然后对本级证书的公钥进行数字摘要算法(证书中提供的指纹加密算法)计算结果,与解密得到的指纹对比。如果一样,说明证书没有被修改过。公钥可以放心使用,可以开始握手通信了。

    操作系统为什么会有证书发布机构的证书?
        证书发布机构除了给别人发布证书外,自己也有自己的证书。
        在操作系统安装好时,受信任的证书发布机构的数字证书就已经被安装在操作系统中了,根据一些权威安全机构的评估,选取一些信誉很好并且通过一定安全认证的证书发布机构,把这些证书默认安装在操作系统中并设为信任的数字证书。
        发布机构持有与自己数字证书对应的私钥,会用这个私钥加密所有他发布的证书及指纹整体作为数字签名。

2、TCP、UDP 和 Socket

TCP:(Transmission Control Protocol )传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议

    三次握手:
        客户端发送 SYN(SEQ=x)报文给服务器端,进入 SYN_SEND 状态。
        服务器端收到 SYN 报文,回应一个 SYN (SEQ=y)ACK(ACK=x+1)报文,进入 SYN_RECV 状态。
        客户端收到服务器端的 SYN 报文,回应一个 ACK(ACK=y+1) 报文,进入 Established 状态。

    TCP 为什么要三次握手?而不是两次或者四次呢?
        为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤。
        如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认。
        如果是四次或者其他,多于三次则是累赘,因为三次已经可以确保双方序列号都已被对方确认。

    四次挥手:
        某个应用进程首先调用 close,称该端执行“主动关闭”(active close)。该端的 TCP 于是发送一个 FIN 分节,表示数据发送完毕。
        接收到这个 FIN 的对端执行 “被动关闭”(passive close),这个 FIN 由 TCP 确认。
        一段时间后,接收到这个文件结束符的应用进程将调用 close 关闭它的套接字。这导致它的 TCP 也发送一个 FIN。
        接收这个最终FIN的原发送端 TCP(即执行主动关闭的那一端)确认这个 FIN。
        既然每个方向都需要一个 FIN 和一个 ACK,因此通常需要4个分节。

UDP:(User Datagram Protocol)用户数据报协议,是一种高速传输和实时性有较高的、无连接的、不可靠的 传输层协议。

TCP 和 UDP 的区别?

1、连接性:TCP 面向连接,UDP 无连接

2、可靠性:TCP 可靠的、保证消息顺序,UDP 不可靠(易丢包)、不能保证顺序

3、模式:TCP 流模式,UDP 数据报格式

4、资源损耗:TCP 更损耗数据

Socket:socket 是 “openwrite/readclose” 模式的一种实现,那么socket 就提供了 这些操作对应的函数接口。使用socket 需要注意:
    心跳的保持
    ping 和 pong 的呼应
    离开页面要断开,进入页面再重新连接
    

Object-C(简称 OC) 语言特性

1、多态

​ 多态表现为了三个方面:动态类型、动态绑定、动态加载。之所以叫做多态,是因为必须到运行时(run time)才会做一些事情。

动态类型:

编译器编译的时候是不能被识别的(如 id 类型),要等到运行时(run time),即程序运行的时候才会根据语境来识别。所以这里面就有两个概念要分清:编译时跟运行时。

动态绑定 :

动态绑定(dynamic binding)貌似比较难记忆,但事实上很简单,只需记住关键词@selector/SEL即可。

而在OC中,其实是没有函数的概念的,我们叫消息机制,所谓的函数调用就是给对象发送一条消息。这时,动态绑定的特性就来了。OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去。这就是动态绑定,要实现他就必须用SEL变量绑定一个方法,最终形成的这个SEL变量就代表一个方法的引用。动态绑定的特定不仅方便,而且效率更高。

动态加载 :

让程序在运行时添加代码模块以及其他资源。用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。可执行代码中可以含有和程序运行时整合的新类。

2、继承

​ OC 不支持多继承,但是可以用 代理(Delegate) 来实现多继承。runtime 消息转发等实现伪多继承 4、代理(Delegate)

​ img

​代理是一种设计模式,以 @protocol 形式体现,一般是一对一传递。

代理为什么用 weak 修饰呢?block和代理的区别?
    ​一般以weak关键词以规避循环引用。
    用 weak 修饰指明该对象并不负责保持delegate这个对象,delegate 这个对象的销毁由外部控制。用 strong 修饰该对象强引用 delegate,外界不能销毁 delegate对象,会导致循环引用。
    block 和代理的区别:
        运行成本:代理运行成本低,block 运行成本高。
        因为block出栈需要将使用的数据从栈内存拷贝到堆内存,如果本身就在堆内存的话计数器会+1,使用完或block置为nil后才消除;
        delegate 只保留了一个指针对象,直接回调,没有额外的消耗。
        写法更简练,更紧凑。
        block 注重结果的传输。
        block 要防止循环引用,善用 __weak 和 __strong。
        公共接口,当方法较多后者调用太频繁建议永 delegate。

5、通知(NSNotificationCenter)

​ 使用观察者模式来实现的用于跨层传递信息的机制。传递方式是一对多的。

如果实现通知机制?

img

6、KVC (Key-value Coding)

键值编码是一种间接访问对象的属性使用字符串来标识属性,而不是通过调用存取方法,直接或通过实例变量访问的机制。非对象类型的变量将被自动封装或者解封成对象,很多情况下会简化程序代码。

KVC 底层实现原理:
当一个对象调用setValue:forKey:方法时,方法内部会做以下操作:
    判断有没有指定 keyset方法,如果有set方法,就会调用 set 方法,给该属性赋值
    如果没有 set 方法,判断有没有跟 key 值相同且带有下划线的成员属性(_key) 如果有,直接给该成员属性进行赋值
    如果没有成员属性 _key ,判断有没有跟key 相同名称的属性。如果有,直接给该属性进行赋值
    如果都没有,就会调用 valueforUndefinedKey 和 setValue:forUndefinedKey: 方法

KVC 使用场景:
    KVC 属性赋值
    添加私有成员变量
    字典和模型之间的互转

7、属性

OC 中,基本数据类型的默认关键字是atomic, readwrite, assign;普通属性的默认关键字是atomic, readwrite, strong。

读写权限:readonlyreadwrite(默认)

原子性: atomic(默认),nonatomic。atomic读写线程安全,但效率低,而且不是绝对的安全,比如如果修饰的是数组,那么对数组的读写是安全的,但如果是操作数组进行添加移除其中对象的还,就不保证安全了。nonatomic禁止多线程,变量保护,提高性能。

引用计数:

    retain/strong:表示指向并拥有该对象。其修饰的对象引用计数会增加1。该对象只要引用计数不为0则不会被销毁。当然强行将其设为nil可以销毁它。

    assign:修饰基本数据类型,修饰对象类型时,不改变其引用计数,会产生悬垂指针,修饰的对象在被释放后,assign指针仍然指向原对象内存地址,如果使用assign指针继续访问原对象的话,就可能会导致内存泄漏或程序异常。这些数值主要存在于栈上。

    weak:不改变被修饰对象的引用计数,所指对象在被释放后,weak指针会自动置为nil,不会造成野指针。比如自定义 IBOutlet 控件属性也是用 weak (因为父控件的 subViews 数组已经对它有了一次强引用)。

    copy:分为深拷贝和浅拷贝
        浅拷贝:对内存地址的复制,让目标对象指针和原对象指向同一片内存空间 会增加引用计数。
        深拷贝:对对象内容的复制,开辟新的内存空间。
        img
        可变对象的copy和mutableCopy都是深拷贝
        不可变对象的copy是浅拷贝,mutableCopy是深拷贝
        copy方法返回的都是不可变对象

8、@property 的本质是什么?ivar 、 setter 、getter 是如何生成并添加到这个类中的?

@property 的本质:
@property = ivar + setter + getter
即:@property 等于声明了ivar(数形变量),并实现了该属性的存取方法(setter + getter)。
@property 作为 OC 的一项特性,主要就在于封装对象中的数据。
OC 通常把其所需要的各种数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。

9、@synthesize 、@dynamic 的区别

@synthesize : 系统会自动生成该属性的 setter 和 getter 方法。
@dynamic : 系统不会自动生成该属性的 setter 和 getter 方法,需要用户自己去实现

10、UIView 和 CALayer 的关系?

UIViewUIView 是 iOS 系统中的界面元素的基础,所有的界面元素都继承自它;
    它本身完全是由 CoreAnimation 来实现的;
    它真正绘图部分,是由 CALayer(CoreAnimation Layer) 类来管理的;
    UIView 本身更像一个 CALayer 的管理器,访问它的根绘图和根坐标有关的属性(如:frame、bounds 等),实际上内部都是在访问他所包含的 CALayer 的相关属性;
    UIView 的属性 layer ,对应的是他的 CALayer 实例。
    UIView 可以响应时间,Layer 不可以,因为 UIView 继承自 UIResponderCALayerCALayer 类似于 UIView 的子 View 树形结构,也可以向它的 layer 上添加子 layer ,来完成某些特殊的表示;
    UIView 的 layer 树形在系统内部分别是:
        逻辑树,这里的代码是可操控的;
        动画树,是一个中间层,系统就在这一层上更改属性,进行各种渲染操作;
        显示树,其内容就是当前正在被显示在屏幕上的内容。
    动画的运作:对 UIView 的 subLayer(非主 Layer)属性进行更改,系统将会自动进行动画生成。
    坐标系统: CALayer 的坐标系统比 UIView 多了一个 anchorPoint 属性,使用 CGPoint 结构标识,值域是 0 ~ 1,是个比例值。
    渲染:当更新层,改变不能立即显示在屏幕上。当所有的层都准备好时,可以调用 setNeedsDisPlay 方法来重绘显示。
    变换:要在一个层中添加一个 3D 或仿射变换,可以分别设置层的 transform 或 affineTransform 属性。
    变形:Quartz Core 的渲染能力,使二维图像可以被自由操纵,就好像是三维的。图像可以在一个三维坐标系中以任意角度被旋转、缩放和倾斜。CATransform3D 的一套方法提供了一些魔术般的变换效果。

11、ViewController 不走 dealloc 的情况

controller 使用了 NSTimer,并未对它进行销毁。
block 块内使用了 self,造成了循环引用。
使用了 delegate,用了 strong 修饰,造成了强持有。记得用 weak/assign 修饰代理。
controller 中使用了 WKWebView,- (void)addScriptMessageHandler:(id)scriptMessageHandlername:(NSString*)name 第一个参数使用self,造成了强持有。解决办法。

12、UICollectionView 自定义 layout 如何实现?

重写 prepareLayout 方法,并在里面事先就计算好必要的布局信息并存储起来。
基于 prepareLayout 方法中的布局信息,重写 collectionViewContentSize 方法返回 UICollectionView的内容尺寸。
重写 layoutAttributesForElementsInRect: 方法返回指定区域 cell、Supplementary View  Decoration View 的布局属性。
重写 layoutAttributesForItemAtIndexPath:;方法返回对应的 indexPath 的位置的 cell 的布局属性。
重写 layoutAttributesForSupplementaryViewOfKind: atIndexPath:,方法返回对应indexPath的位置的追加视图的布局属性,如果没有就不用重载。
重写 layoutAttributesForDecorationViewOfKind: atIndexPath:,方法返回对应indexPath的位置的装饰视图的布局属性,如果没有也不需要重载。
重写 shouldInvalidateLayoutForBoundsChange:,当边界发生变化时,是否应该刷新。

13、AppDelegate 的生命周期?从后台到前台调用了哪些方法?从前台到后台调用了哪些方法?第一次启动调用了哪些方法

生命周期:
    当应用程序启动时(不包括已在后台的情况下转到前台),调用此回调。 launchOptions 是启动参数,假如用户通过点击push通知启动的应用,这个参数里会存储一些push通知的信息。

    – (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions  {  
        NSLog(@"程序载入后");  
    }  

    应用程序将要进入非活动状态执行(一般在程序运行时,有来电,锁屏,按HOME键,下拉通知栏,双击HOME键等情况会调用此方法),在此期间,应用程序不接受消息或事件 。在此方法中可以暂停正在进行的任务,如禁用定时器,暂停游戏等。

    - (void)applicationWillResignActive:(UIApplication *)application  {  
        NSLog(@"应用程序将要进入非活动状态(进入后台)");  
    }  


    应用程序已经进入后台运行(应用程序支持后台运行),使用此方法来释放资源共享,保存用户数据,无效计时器,并储存足够的应用状态信息,等应用重新进入前台运行时将应用恢复到目前的状态。

    - (void)applicationDidEnterBackground:(UIApplication *)application  {  
        NSLog(@"应用程序已经进入后台运行");  
    }  


    应用程序将要进入活动状态执行,若应用不在后台状态,而是直接启动,则不会回调此方法。

    - (void)applicationWillEnterForeground:(UIApplication *)application  {  
        NSLog(@"应用程序将要进入前台运行");  
    }  


    应用程序已经进入活动状态,即当应用程序重新启动,或者在后台转到前台,完全激活时,都会调用这个方法。

    - (void)applicationDidBecomeActive:(UIApplication *)application  {  
        NSLog(@"应用程序已进入前台,处于活动状态");  
    }  


    当应用程序使用了太多的内存,操作系统会终止应用程序的运行,在终止前会调用这个方法。通常可以在这里进行内存清理工作,如释放一些当前不显示的页面,防止程序被终止。

    -(void)applicationDidReceiveMemoryWarning:(UIApplication *)application  {  
        NSLog(@"系统内存不足,需要进行清理工作");  
    }  

    应用程序将要退出,且进程即将结束时会调用这个方法,一般很少主动调用,更多是内存不足时是被迫调用的,我们应该在这个方法里做一些数据存储操作和一些退出前的清理工作。

    - (void)applicationWillTerminate:(UIApplication *)application  {  
        NSLog(@"应用程序将要退出");  
    }  


    当系统时间发生改变时执行,应用中一些依赖系统时间的配置,需要在此方法中作相应改变。

    -(void)applicationSignificantTimeChange:(UIApplication *)application  {  
        NSLog(@"系统时间发生改变");  
    }  

后台到前台:
    应用程序将要进入活动状态,调用 applicationWillEnterForeground: 。
    应用程序已经进入活动状态,调用 applicationDidBecomeActive 。
前台到后台:
    应用程序将要进入非活动状态,调用 applicationWillResignActive 。
    应用程序已经进入后台运行,调用 applicationDidEnterBackground 。
首次启动调用方法:
    先调用 application: didFinishLaunchingWithOptions: 方法
    调用 applicationDidBecomeActive ,应用程序已经进入活动状态。

14、NSCache 优于 NSDictionary 的几点?

NSCache 是一个非常奇怪的集合。默认为可变并且线程安全的。这使它很适合缓存那些创建起来代价高昂的对象。它自动对内存警告做出反应并基于可设置的成本清理自己。与NSDictionary相比,键是被retain而不是被拷贝的。

当系统资源将要耗尽时,NSCache可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统通知时手动删减缓存,NSCache会先行删减时间最久为被使用的对象。
NSCache 并不会拷贝键,而是会保留它。此行为用NSDictionary也可以实现,但是需要编写比较复杂的代码。NSCache对象不拷贝键的原因在于,很多时候键都是不支持拷贝操作的对象来充当的。因此NSCache对象不会自动拷贝键,所以在键不支持拷贝操作的情况下,该类比字典用起来更方便。
NScache是线程安全的,NSDictionary不是。在开发者自己不编写加锁代码的前提下,多个线程可以同时访问NSCache。对缓存来说,线程安全通常是很重要的,因为开发者可能在某个线程中读取数据,此时如果发现缓存里找不着指定的键,那么就要下载该键对应的数据了。

15、id 和 instanceType 有什么区别?

相同点:
instancetype 和 id 都是万能指针,指向对象。
不同点:

id 在编译的时候不能判断对象的真实类型,instancetype 在编译的时候可以判断对象的真实类型。
id可以用来定义变量,可以作为返回值类型,可以作为形参类型;instancetype 只能作为返回值类型。

16、self 和 super 的区别 ?

self 调用自己方法,super 调用父类方法
self 是类,super 是预编译指令
[self class] 和 [super class] 输出是一样的
self和super底层实现原理

当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
而当使用 super 时,则从父类的方法列表中开始找,然后调用父类的这个方法;
当使用 self 调用时,会使用 objc_msgSend 函数:

id objc_msgSend(id theReceiver, SEL theSelector, ...)
    1

第一个参数是消息接收者,第二个参数是调用的具体类方法的 selector,后面是 selector 方法的可变参数。以 [self setName:] 为例,编译器会替换成调用 objc_msgSend 的函数调用,其中 theReceiver 是 self,theSelector 是 @selector(setName:),这个 selector 是从当前 self 的 class 的方法列表开始找的 setName,当找到后把对应的 selector 传递过去。
当使用 super 调用时,会使用 objc_msgSendSuper 函数:

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
    1

第一个参数是个objc_super的结构体,第二个参数还是类似上面的类方法的selector

struct objc_super {
	id receiver;
	Class superClass;
};

17、setNeedsDisplay 和 layoutIfNeeded 两者是什么关系?

UIView 的 setNeedsDisplay 和 setNeedsLayout 两个方法都是异步执行的。
    setNeedsDisplay 会自动调用 drawRect方法,这样可以拿到 UIGraphicsGetCurrentContext 进行绘制;
    setNeedsLayout 会默认调用 layoutSubViews ,给当前的视图做了标记;layoutIfNeeded 查找是否有标记,如果有标记及立刻刷新。
    只有 setNeedsLayout 和 layoutIfNeeded 这二者合起来使用,才会起到立刻刷新的效果。

Swift

1、Swift 和 OC

swift 和 OC 的联系
    swift 和 OC 共用一套运行时环境,swift 和 OC 可以互相桥接,互相引用混合编程;
    OC 中很多类库,在 swift 中依然可以直接使用,只是语法上有些改变;
    OC 中的计数器、ARC、属性、协议、接口、初始化、扩展类、命名参数、匿名函数等绝大多数概念,在 swift 中继续有效。
    swift 中有 OC 没有的一些概念。比如:元组 , 泛型 ,函数式编程模式(如 mapfilter、reduce 等)等。
swift 相对于 OC 的优势
    swift 容易阅读,语法和文件结构简洁化。
    swift 更容易维护,文件分离后结构更清晰。
    swift 更加安全,它是类型安全的语言。
    swift 代码更少,语法更简洁,可以省去大量冗余的代码。
    swift 速度更快,运算性能更高。

语言 Swift 优点 1. 语法更简洁 2. 报错精准(报错的时候直接显示报错行)3. 定义变量简单(定义变量不用区分整型,浮点型等等,变量使用var,常量使用let。)4. 可视化互动效果(开发工具带来了Xcode Playgrounds功能,该功能提供强大的互动效果,能让Swift源代码在撰写过程中实时显示出其运行结果。) 5. 函数式编程的支持(Swift 语言本身提供了对函数式编程的支持;Objc 本身是不支持的,通过引入 ReactiveCocoa 这个库才可支持函数式编程。) 缺点 1. Swift目前还没有得到全面性的推广 2. Swift 暂时还不稳定,在 Swift 5.0 之前 API 不稳定,之后变得稳定 3. 第三方库的支持不够多 4. App体积变大( App 体积大概增加 5-8 M 左右)5. 上线方式改变(在上线的时候,不能使用application Loader上传包文件,会提示你丢失了swift support files,应该使用xcode直接上传。) 2、Swift 的可选项类型(Optionals)

swift 引用了可选项类型,用于处理变量值不存在的情况。 Optionals 类似于 OC 中指向 nil 的指针,但是适用于所有的数据类型,而非仅仅局限于类,Optionals 相比于 OC 中的 nil 指针 ,更加安全和简明,并且也是 swift 诸多最强大功能的核心。 3、Swift 中的 struct 和 class

相比于 OC 中的结构体,Swift 对结构体的使用比重大了很多,结构体成了实现面向对象的重要工具。
相比于 C++ 和 OC 中的结构体只能定义一组相关的成员变量,在 Swift 不仅可以定义成员变量(属性),还可以定义成员方法。 因此在 Swift 中,我们可以把结构体看做是一种轻量级的类。
Swift 中结构体不具有继承性,也不具备运行时类型强制转换、使用析构器和使用引用计等能力。
Swift 中 struct 是值类型,而 class 是引用类型。
值类型的变量直接包含他们得数据,而引用类型的变量存储对他们的数据引用。
因此引用类型的变量被称为对象,因此对一个变量操作可能影响另一个变量所引用的对象。
而对于值类型都有他们自己的数据副本,因此对一个值类型的变量操作不可能影响到另一个值类型的变量。

4、swift 中 defer、guard?

defer
defer 关键字提供了一个安全和简便的方式来处理这件事,当离开当前的代码块时,会执行defer对应的代码块

func openFileAction(){
    ///打开文件
    openFile()
    defer{
        closeFile()
    }
    ///读文件
    let isRead = readFile()
    guard isRead else {
        return
    }

    if emptyFile() {
        return
    }
    print("读取成功")
}


guard
guard 当条件满足的时候,会顺序执行,如果 guard条件不满足的时候,会进入 guard 内部,并执行 return 操作,终止代码的执行

5、Swift 中高阶函数有哪些?

map
用于映射, 可以将一个列表转换为另一个列表
数组元素类型转换

//swift为函数的参数自动提供简写形式,$0代表第一个参数,$1代表第二个参数
let array = ["1", "2", "3"]
let str1 = array.map({ "\($0)"}) //数组每个元素转成String类型
//字符串数组转NSInteger类型数组
let array1 = array.map { (obj) -> NSInteger in
    return NSInteger(obj) ?? 0
}
//NSInteger类型数组转字符串数组
let array2 = array1.map { (obj) -> String in
    return String(obj)
}
print("array1: \(array1)")
print("array2: \(array2)")
//str1 ["1", "2", "3"]
//array1: [1, 2, 3]
//array2: ["1", "2", "3"]


flatMap
功能跟map类似; 区别是flatMap会过滤nil元素, 并解包Optional
flatMap 还可以将多维数组转换为一维数组,对于N维数组, map函数仍然返回N维数组

let array = [[1, 2, 3],[1, 2, 3],[1, 2, 3]]
let arrret = array.flatMap{$0}
let arrret1 = array.map{$0}
print(arrret)
print(arrret1)
//[1, 2, 3, 1, 2, 3, 1, 2, 3]
//[[1, 2, 3], [1, 2, 3], [1, 2, 3]]

filter
用于过滤, 可以筛选出想要的元素

let array = [1, 2, 3]
let resultArray = array.filter { return $0 > 1 }
print(resultArray)
//[2, 3]


reduce
reduce 方法把数组元素组合计算为一个值

//我们要求和
let numbers = [2, -5, 9, 7, -2, 5, 3, 1, 0, -3, 8]
//传统
var result = 0
for x in numbers {
    result += x
}
//使用reduce
result = numbers.reduce(0,{$0+$1})

6、Swift 为什么将String,Array,Dictionary设计成值类型?

值类型相比引用类型,最大的优势在于内存使用的高效。值类型在栈上操作,引用类型在堆上操作。栈上的操作仅仅是单个指针的上下移动,而堆上的操作则牵涉到合并、移位、重新链接等。也就是说Swift这样设计,大幅减少了堆上的内存分配和回收的次数。同时copy-on-write又将值传递和复制的开销降到了最低。
StringArrayDictionary设计成值类型,也是为了线程安全考虑。通过Swiftlet设置,使得这些数据达到了真正意义上的“不变”,它也从根本上解决了多线程内存访问和操作的问题。
设计成值类型还可以提升API的灵活度。例如通过实现Collection这样的协议,我们可以遍历String,使得整个开发更加灵活高效。

7、Swift 中的 async/await? (swift 5.5 后,百度问)

async:
表示这个函数时可以异步执行的,也就是说执行这段代码是可以不阻塞当前线程。
    函数/方法可以是异步的,属性也可以是异步的。
    当您将函数标记为异步时,您就允许它挂起。当一个函数挂起自己时,它也会挂起它的调用者。所以它的调用者也必须是异步的。
    为了指出异步函数中它可能挂起一次或多次的位置,使用了 await 关键字。
    当异步函数被挂起时,线程不会被阻塞。
    当异步函数恢复时,从它调用的异步函数返回的结果流回原始函数,并从上次停止的地方继续执行。

await:
在函数、属性和初始值设定项中,await 可用于表达式可以解除当前线程阻塞;除此之外,await 还可以用于异步序列。

8、Swift 消息派发机制有几种?详细说说。

Swift 中派发机制分为直接派发、函数表派发、消息派发 三种。

直接派发 (Direct Dispatch):
直接派发是最快的, 不止是因为需要调用的指令集会更少, 并且编译器还能够有很大的优化空间。 例如函数内联等。 直接派发也有人称为静态调用。然而,对于编程来说直接调用也是最大的局限,而且因为缺乏动态性所以没办法支持继承。

函数表派发 (Table Dispatch):
函数表派发是编译型语言实现动态行为最常见的实现方式。
函数表使用了一个 数组来存储类声明的每一个函数的指针。大部分语言把这个称为 “virtual table”(虚函数表),Swift 里称为 “witness table”。每一个类都会维护一个函数表,里面记录着类所有的函数,如果父类函数被 override 的话,表里面只会保存被 override 之后的函数。一个子类新添加的函数,都会被插入到这个数组的最后。运行时会根据这一个表去决定实际要被调用的函数。

消息派发 (Message Dispatch): Object-c的OO实现
消息机制是调用函数最动态的方式。也是 Cocoa 的基石,这样的机制催生了 KVO,UIAppearence 和 CoreData 等功能。这种运作方式的关键在于开发者可以在运行时改变函数的行为。不止可以通过 swizzling 来改变,甚至可以用 isa-swizzling 修改对象的继承关系, 可以在面向对象的基础上实现自定义派发。

swift 消息派发如图:做了总结

派发的使用场景:
    值类型使用直接派发。
    class和协议的extension使用的是直接派发。
    class和协议的初始化声明使用的是函数表派发。
    class@obj extension 使用的是消息机制派发。
指定派发方式:

    finalfinal 允许类里面的函数使用直接派发。这个修饰符会让函数失去动态性。任何函数都可以使用这个修饰符,就算是 extension 里本来就是直接派发的函数。
        这也会让 Objective-C 的运行时获取不到这个函数, 不会生成相应的 selector。

    dynamicdynamic 可以让类里面的函数使用消息机制派发。
        使用 dynamic, 必须导入 Foundation 框架,里面包括了 NSObject 和 Objective-C 的运行时。
        dynamic 可以让声明在 extension 里面的函数能够被 overridedynamic 可以用在所有 NSObject 的子类和 Swift 的原声类。这就是为什么KVO的属性需要用dynamic修饰。

    @objc :
    函数能被 Objective-C 的运行时捕获到。
    使用 @objc 的典型例子就是给 selector 一个命名空间 @objc(abc_methodName),让这个函数可以被 Objective-C 的运行时调用。

    @nonobjc:
    禁止消息机制派发这个函数,不让这个函数注册到 Objective-C 的运行时里。

    @inline:直接派发。

其他

1、什么是静态库?什么是动态库?有什么区别?

库的本质是可执行的二进制文件,是资源文件和代码编译的一个集合。根据链接方式不同,可以分为动态库和静态库,其中系统提供的库都属于动态库。

静态库:
静态库形式:.a.framework,作用是在进行链接生成可执行文件时,从静态库文件中拷贝需要的内容到最终的可执行文件中。
被多次使用就有多份冗余拷贝。

//在使用gcc编译时采用 -static选项来进行静态文件的链接:
gcc -c main.c
gcc -static -o main main.o


动态库:
静态库形式: .dylib.framework ,并不在链接时将需要的二进制代码都拷贝到可执行文件中,而是拷贝一些重定位和符号表信息,当程序运行时需要的时候再通过符号表从动态库中获取(动态加载)。 系统只加载一次,多个程序共用,节省内存。

动静态库区别:
库名称	优点	缺点
静态库	1.目标程序没有外部依赖,直接就可以运行。2. 效率教动态库高。	1. 会使用目标程序的体积增大。因为它将需要用到的代码从二进制文件中拷贝了一份
动态库	1. 不需要拷贝到目标程序中,不会影响目标程序的体积。2. 同一份库可以被多个程序使用(因为这个原因,动态库也被称作共享库)。3. 编译时才载入的特性,也可以让我们随时对库进行替换,而不需要重新编译代码。实现动态更新。	1. 动态载入会带来一部分性能损失(可以忽略不计)2.动态库也会使得程序依赖于外部环境。如果环境缺少动态库或者库的版本不正确,就会导致程序无法运行(Linux lib not found 错误)。

原文链接:blog.csdn.net/qq_33336219…