文章主要会涉及如下几个问题:

  1. if-else 和 switch-case 两者相比谁的效率会高些?在日常开发中该如何抉择?
  2. 如何基于赫夫曼树结构减少 if-else 分支判断次数?
  3. 如何巧妙的应用 do...while(0) 改善代码结构?
  4. 哨兵是什么东西?如何利用哨兵提高有序数组查找效率?
  5. 如何降低 for 循环嵌套的时间复杂度?
  6. 如何利用策略模式替换繁琐的 if-else 分支?

一、if-else 和 switch-case 效率问题

switch-case 与 if-else 的根本区别:

switch 会生成一个跳转表来指示实际的 case 分支的地址,而这个跳转表的索引号与 switch 变量的值是相等的。

所以 switch-case 不用像 if-else 那样遍历条件分支直到命中条件,只需访问对应索引号的表项从而到达定位分支。

具体地说,switch-case 会生成一份大小(表项数)为最大 case 常量 +1 的跳转表,程序首先判断 switch 变量是否大于最大 case 常量,若大于,则跳到 default 分支处理;否则取得索引号为 switch 变量大小的跳表项的地址(即跳表的起始地址+表项大小 * 索引号),程序接着跳到此地址执行,到此完成了分支的跳转。

  1. int main() {
  2. unsigned int i, j;
  3. i = 3;
  4. switch (i) {
  5. case 0:
  6. j = 0;
  7. break;
  8. case 1:
  9. j = 1;
  10. break;
  11. case 2:
  12. j = 2;
  13. break;
  14. case 3:
  15. j = 3;
  16. break;
  17. case 4:
  18. j = 4;
  19. break;
  20. default:
  21. j = 10;
  22. break;
  23. }
  24. }

用 gcc 编译器,生成汇编代码(不开编译器优化)

  1. _main: ## @main
  2. Lfunc_begin0:
  3. .loc 1 12 0 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:12:0
  4. .cfi_startproc
  5. ## %bb.0:
  6. pushq %rbp
  7. .cfi_def_cfa_offset 16
  8. .cfi_offset %rbp, -16
  9. movq %rsp, %rbp
  10. .cfi_def_cfa_register %rbp
  11. movl $0, -4(%rbp)
  12. Ltmp0:
  13. .loc 1 14 7 prologue_end ## /Users/cykj/Desktop/Demo/Demo/MyC.c:14:7
  14. movl $3, -8(%rbp)
  15. .loc 1 16 13 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:16:13
  16. movl -8(%rbp), %eax
  17. movl %eax, %ecx
  18. movq %rcx, %rdx
  19. subq $4, %rdx
  20. .loc 1 16 5 is_stmt 0 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:16:5
  21. movq %rcx, -24(%rbp) ## 8-byte Spill
  22. movq %rdx, -32(%rbp) ## 8-byte Spill
  23. ja LBB0_6
  24. ## %bb.8:
  25. .loc 1 0 5 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:0:5
  26. leaq LJTI0_0(%rip), %rax
  27. movq -24(%rbp), %rcx ## 8-byte Reload
  28. movslq (%rax,%rcx,4), %rdx
  29. addq %rax, %rdx
  30. jmpq *%rdx
  31. LBB0_1:
  32. Ltmp1:
  33. .loc 1 18 15 is_stmt 1 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:18:15
  34. movl $0, -12(%rbp)
  35. .loc 1 19 13 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:19:13
  36. jmp LBB0_7
  37. LBB0_2:
  38. .loc 1 22 15 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:22:15
  39. movl $1, -12(%rbp)
  40. .loc 1 23 13 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:23:13
  41. jmp LBB0_7
  42. LBB0_3:
  43. .loc 1 26 15 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:26:15
  44. movl $2, -12(%rbp)
  45. .loc 1 27 13 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:27:13
  46. jmp LBB0_7
  47. LBB0_4:
  48. .loc 1 30 15 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:30:15
  49. movl $3, -12(%rbp)
  50. .loc 1 31 13 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:31:13
  51. jmp LBB0_7
  52. LBB0_5:
  53. .loc 1 34 15 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:34:15
  54. movl $4, -12(%rbp)
  55. .loc 1 35 13 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:35:13
  56. jmp LBB0_7
  57. LBB0_6:
  58. .loc 1 38 15 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:38:15
  59. movl $10, -12(%rbp)
  60. Ltmp2:
  61. LBB0_7:
  62. .loc 1 42 1 ## /Users/cykj/Desktop/Demo/Demo/MyC.c:42:1
  63. movl -4(%rbp), %eax
  64. popq %rbp
  65. retq
  66. Ltmp3:
  67. Lfunc_end0:
  68. .cfi_endproc
  69. .p2align 2, 0x90
  70. .data_region jt32
  71. L0_0_set_1 = LBB0_1-LJTI0_0
  72. L0_0_set_2 = LBB0_2-LJTI0_0
  73. L0_0_set_3 = LBB0_3-LJTI0_0
  74. L0_0_set_4 = LBB0_4-LJTI0_0
  75. L0_0_set_5 = LBB0_5-LJTI0_0
  76. LJTI0_0:
  77. .long L0_0_set_1
  78. .long L0_0_set_2
  79. .long L0_0_set_3
  80. .long L0_0_set_4
  81. .long L0_0_set_5
  82. .end_data_region
  83. ## -- End function

由此看来,switch 有点以空间换时间的意思,而事实上也的确如此。

  1. 当分支较多时,当时用 switch 的效率是很高的。因为 switch 是随机访问的,就是确定了选择值之后直接跳转到那个特定的分支,但是 if-else 是遍历所有的可能值,直到找到符合条件的分支。

    但不总是那么好,因为每次计算会有一个二次查表过程。 具体需要看应用场景,举个例子:对于网络层的协议分析,99% 可能都是 IP 协议,因此基本上会在第一个 if 时就命中,只有一次计算。

    总结:对于分支较多或分布相对均匀的情况,使用 switch 可以提高效率;对于分支较少或分布不均匀的情况,使用 if-else 更好。

  2. 由上面的汇编代码可知道,switch-case 占用较多的代码空间,因为它要生成跳转表,特别是当 case 常量分布范围很大但实际有效值又比较少的情况,switch-case 的空间利用率将变得很低。

  3. switch-case 只能处理 case 为常量的情况,对非常量的情况是无能为力的。例如 if (a > 1 && a < 100),是无法使用 switch-case 来处理的。所以 if-else 能应用于更多的场合,比较灵活。

文章:switch 与 if-else 的效率问题

二、用 do-while(0) 改善代码结构

先看一段代码,要重点注意代码中的注释。

  1. - (NSString *)handleString:(NSString *)str
  2. {
  3. if (![str isKindOfClass:[NSString class]]) {
  4. return nil;
  5. }
  6. if(str.length <= 0) {
  7. return nil;
  8. }
  9. // 第一部分逻辑依赖于前面的判断,只有判断通过的时候才执行
  10. code1...code1
  11. // 第二部分逻辑不依赖于前面的判断(第二部分中的逻辑可能会依赖第一部分逻辑处理结果),无论判断是否通过都要执行
  12. code2...code2
  13. }

试问,怎样做才能巧妙的满足上述注释代码的需求,因为上述代码中存在 return nil,一旦执行到此处,逻辑一和逻辑二处的伪代码都不会再执行。为了满足上述要求,我们可以巧妙的利用 break 退出临时构造的代码块,但不退出整个函数。

  1. - (NSString *)handleString:(NSString *)str {
  2. do {
  3. if (![str isKindOfClass:[NSString class]]) {
  4. break;
  5. }
  6. if(str.length <= 0) {
  7. break;
  8. }
  9. // 第一部分逻辑依赖于前面的判断,只有判断通过的时候才执行
  10. code1...code2
  11. } while (0);
  12. // 第二部分逻辑不依赖于前面的判断(第二部分中的逻辑可能会依赖第一部分逻辑处理结果),无论判断是否通过都要执行
  13. code2...code2
  14. }

三、有序数组查找操作中的哨兵

正常的查找处理。

  1. NSArray *arr = @[@1, @2, @3, @4, @5];
  2. for (NSInteger i = 0; i < arr.count; i++) {
  3. if ([arr[i] integerValue] == 2) {
  4. NSLog(@"for 找到了");
  5. }
  6. }

利用哨兵进行查找处理。

  1. - (BOOL)search:(NSNumber *)key array:(NSMutableArray *)arr
  2. {
  3. if (arr.count <= 0) {
  4. return NO;
  5. }
  6. NSNumber * firstObj = (NSNumber *)arr[0];
  7. if ([firstObj integerValue] == [key integerValue]) {
  8. return YES;
  9. }
  10. NSInteger i = arr.count - 1;
  11. NSLock * lock = [[NSLock alloc] init];
  12. [lock lock];
  13. arr[0] = key;
  14. // 同上面 for 循环相比,i < arr.count 的判断,在处理大批量数据时候,对性能提升比较大
  15. while ([arr[i] integerValue] != [key integerValue]) {
  16. i--;
  17. }
  18. arr[0] = firstObj;
  19. [lock unlock];
  20. return (i != 0);
  21. }

仔细观察上述两段代码,同样是在有序数组中查找目标为 2 的元素,第一段代码是常规迭代处理,第二段代码是将要查找的元素设置为哨兵。同第一段代码相比第二种方式少了 i < arr.count 的判断,在小批量有序数组查询中对效率的提升并无明显影响,但是在处理大批量数据时候,对性能提升还是比较明显的。

四、多层 for 嵌套处理

实际开发中应尽量避免使用双层 for 循环,客户端数据量比较小可能实际开发中并不是很注意这些。但是后端开发过程中,数据量比较大, 为了提升性能,有些公司后端开发中可能会直接规定避免使用多层 for 循环嵌套的形式。一般第二层或更深层的 for 循环可以使用字典替换。双层 for 循环嵌套的时间复杂度是 n 的二次方。但如果内部 for 循环用字典代替时间复杂度为 O(2n)(实际是 O(n))。如:两个数组中有且只有一个相同元素,寻找该元素。其中一个数组就可以先用字典做保存,遍历第一个数组的时候,同字典中的数据做比较即可。

  1. NSArray *arr1 = @[@1, @2, @3, @4, @5];
  2. NSArray *arr2 = @[@5, @6, @7, @8];
  3. NSMutableDictionary * dict = [NSMutableDictionary dictionary];
  4. for (NSInteger i = 0; i < arr2.count; i++) {
  5. [dict setObject:arr2[i] forKey:[NSString stringWithFormat:@"%ld", i]];
  6. }
  7. for (NSInteger i= 0 ; i < arr1.count; i++) {
  8. NSNumber * number = [dict objectForKey:[NSString stringWithFormat:@"%ld", i]];
  9. if ([arr1[i] integerValue] == [number integerValue]) {
  10. NSLog(@"相同的数据为:%@", number);
  11. break;
  12. }
  13. }

五、用策略模式替换 if-else

https://www.jianshu.com/p/98fa80eebc52

六、内容来源

用if else,switch,while,for颠覆你的编程认知

if-else、switch、while、for的更多相关文章

  1. Java语法基础(三)----选择结构的if语句、switch语句

    [前言] 流程控制语句: 在一个程序执行的过程中,各条语句的执行顺序对程序的结果是有直接影响的.也就是说程序的流程对运行结果有直接的影响.所以,我们必须清楚每条语句的执行流程.而且,很多时候我们要通过 ...

  2. 五、Java基础---------if else、switch总结

    在前几篇博客中主要是以笔者遇到的一些典型的题目为例子而展开的讨论,接下来几篇将是以知识点的结构进行讲述.本文主要是讲述if ()else .if() else if().switch() case 的 ...

  3. 语句:分支语句、switch case ——7月22日

    语句的类型包括:声明语句.表达式语句.选择语句.循环语句.跳转语句.异常语句 1.声明语句引:入新的变量或常量. 变量声明可以选择为变量赋值. 在常量声明中必须赋值. 例如: int i = 0;// ...

  4. Interview----求 1+2+...+n, 不能用乘除法、for、while if、else、switch、case 等关键字以及条件判断语句 (A?B:C)

    题目描述: 求 1+2+...+n, 要求不能使用乘除法.for.while.if.else.switch.case 等关键字以及条件判断语句 (A?B:C). 分析: 首先想到的是写递归函数,但是遇 ...

  5. 设置ToggleButton、Switch、CheckBox和RadioButton的显示效果

    ToggleButton.Switch.CheckBox和RadioButton都是继承自android.widget.CompoundButton,意思是可选择的,因此它们的用法都很类似.Compo ...

  6. 选择结构if、switch

    选择结构if.switch 一.if三种形式 if if-else 3.if -else if -else 二.switch 针对某个表达式的值做出判断,成为决定执行代码块 switch 语句特点: ...

  7. R语言︱函数使用技巧(循环、if族/for、switch、repeat、ifelse、stopifnot)

    每每以为攀得众山小,可.每每又切实来到起点,大牛们,缓缓脚步来俺笔记葩分享一下吧,please~ --------------------------- 后续加更内容: 应用一:if族有哪些成员呢?- ...

  8. Golang 笔记 3 if、switch、for、select语句

    一.if语句 Go的流程控制主要包括条件分支.循环和并发.  if语句一般由if关键字.条件表达式和由花括号包裹的代码块组成.在Go中,代码块必须由花括号包裹.这里的条件表达式是结果类型为bool的表 ...

  9. Java运算符、switch、数组、排序

    1.Java的运算符,分为四类:算数运算符.关系运算符.逻辑运算符.位运算符 运算符例子:22.25(十进制转化为二进制,8421码)0010 0010 (22)0010 0101 (25) 位运算符 ...

  10. 《Go学习笔记 . 雨痕》流程控制(if、switch、for range、goto、continue、break)

    Go 精简(合并)了流控制语句,虽然某些时候不够便捷,但够用. if...else... 条件表达式值必须是布尔类型,可省略括号,且左花括号不能另起一行. func main() { x := 3 i ...

随机推荐

  1. java快速开发平台可视化开发表单

    XJR java快速开发平台,简单的理解就是:开发人员以某种编程语言或者某几种编程语言(比如:目前流行的多种web技术,包括springboot, JPA,Druid, Activiti,Lombok ...

  2. sql -- 利用order by 排名作弊

    表结构: 需求: 方法1:union ,,) order by user_total desc ) a union (,,) order by user_total desc ) b) 方法2:直接在 ...

  3. JDK 1.8 新特性之Date-Time API

    来源:请点击查看 1.8之前的日期类: 线程不安全:java.util.Date 这个类线程不安全,而且所有日期类都是可变的. 时间处理麻烦:默认的开始日期从1900年,不支持国际化,不提供时区支持, ...

  4. 教你如何使用css隐藏input的光标

    今天公司的ui突然跑过来问我一个问题:"如何在不影响操作的情况下,把input的光标隐藏了?". 我相信很多人会跟我一样,觉得这是个什么狗屁需求,输入框不要光标这不是反人类吗?可惜 ...

  5. mysql从5.5升级到5.7遇到的坑

    在安装mysql5.7时很顺利安装完成,但在启动项目时报错: [Err] 1055 - Expression #1 of ORDER BY clause is not in GROUP BY clau ...

  6. linux yum安装MySQL5.6

    1.新开的云服务器,需要检测系统是否自带安装mysql # yum list installed | grep mysql 2.如果发现有系统自带mysql,果断这么干 # yum -y remove ...

  7. 06 EntityManager和EntityTransaction

    EntityManager 在 JPA 规范中, EntityManager是完成持久化操作的核心对象.实体类作为普通 java对象,只有在调用 EntityManager将其持久化后才会变成持久化对 ...

  8. .net core WebAPI+EF 动态接收前台json,并动态修改数据库

    用API开发的人都知道,常用的后台接收参数就是建个DTO,然后前台把这个DTO传过来.后台再更新,例如如下例子: public async Task<IActionResult> PutM ...

  9. (转)协议森林05 我尽力 (IP协议详解)

    协议森林05 我尽力 (IP协议详解) 作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! IPv4与IPv6头部的对比 我们已经在I ...

  10. 大数据软件安装之Flume(日志采集)

    一.安装地址 1) Flume官网地址 http://flume.apache.org/ 2)文档查看地址 http://flume.apache.org/FlumeUserGuide.html 3) ...