James Aspnes
2025-04-27T02:23:25-0400
This is the course information for CPSC 223:Data Structures and Programming Techniques for the Spring 2022 semester. This document is available in HTML format at the URLhttps://www.cs.yale.edu/homes/aspnes/classes/223/notes.html.
Code examples can be downloaded from links in the text, or can be found in theexamples directory.
This document is a work in progress, and is likely to change frequently as the semester progresses.
Copyright © 2001–2021 by James Aspnes. Distributed under a Creative Commons Attribution-ShareAlike 4.0 International license:https://creativecommons.org/licenses/by-sa/4.0/.
Please feel free to send questions or comments on the class or anything connected to it tojames.aspnes@gmail.com.
K&R refers to the Kernighan and Ritchie book. Examples from lecture can be found in theexamples directory under2022/lecture if the links below have not been updated yet.
main function, program structure, basic integer data types. Reading:Course administration,The Zoo,The Linux programming environment, a little bit aboutdeveloping on your own machine,Structure of a C program,Basic integer types; K&R §§1.1, 1.2.Examples from lecture:hello.c.if,while,for,do..while. Relational and logical operators:<,<=,==,!=,>=,>,&&,||,!,?:, and,. More assignment operators:+= and friends,++ and--. Reading:Statements; K&R §§1.5, 2.6, 2.8, 2.10, 2.12, 3.1, 3.2, 3.3, 3.5, and 3.6.getchar,putchar, andungetc. Goto-like statements:switch,break,continue, andgoto. Reading:Reading and writing single characters; K&R §§1.5, 3.7, and 3.8.& and* operators. Using a pointer to get a value out of a function. Array declarations. Reading:Pointers up throughArrays; K&R 5.1–5.4. Examples from lecture:pointers.c.char * vs. const char *,argv, various pointer hackery involving* and++. Reading:Strings; K&R 5.5. Examples from lecture:strings.c.malloc,calloc,free, andrealloc. Thevoid * pointer type. Finding storage allocation bugs usingvalgrind. Reading:Run-time storage allocation usingmalloc,Valgrind.Examples from lecture. The easiest way to compile all of these is to download everything then runmake all ormake test.realloc to deal with data of unknown size. Basics of debugging withgdb andvalgrind. Reading:Debugging,Dynamic arrays for a bit more about use ofrealloc.Examples from lecture.structs,unions, andenums. Reading:Structured data types; K&R Chapter 6, §2.5 (forenums).make. Thestdio.h library and file I/O. Reading:Function declarations and modules,Static functions,Make,File I/O.Examples from lecture.qsort, dispatch tables, callbacks, closures. Reading:Function pointers; K&R §5.11.Examples from lecture.treeSize andtreeDepth functions). Timing and profiling. Optimizing for performance. Reading:Augmented trees,Performance tuning.Examples from lecture.Computer Science 223b, Data Structures and Programming Techniques.
Instructor: James Aspnes.
On-line information about the course, including the lecture schedule, lecture notes, and information about assignments, can be found athttps://www.cs.yale.edu/homes/aspnes/classes/223/notes.html. This document will be updated frequently during the semester, and is also available inPDF format.
Lectures are MW 13:00–14:15 in the 53 Wall Street Auditorium (WALL53 AUD).
Thelecture schedule can be found in the course notes. Acalendar is also available.
Topics include programming in C; data structures (arrays, stacks, queues, lists, trees, heaps, graphs); sorting and searching; storage allocation and management; data abstraction; programming style; testing and debugging; writing efficient programs.
CPSC 201, or equivalent background. See me if you aren’t sure.
The textbook for this course is:
If you are on the Yale campus or are using VPN to get to Yale’s network, you can access this book athttp://proquest.safaribooksonline.com/book/programming/c/9780133086249. You do not need to buy a physical copy of this book unless you want to.
Twelve homework assignments. For each student, the lowest two assignment grades will be dropped, and the remaining ten assignments will together count for 100% of the semester grade. Due to the size of the class and the issues of managing exams during an ongoing pandemic, there will be no exams.
See thecalendar for open office hours.
James Aspnes (james.aspnes@gmail.com,http://www.cs.yale.edu/homes/aspnes/). Office hours are listed in the calendar on my home page. If my open office hours don’t work for you, please send email to make an appointment.
Students are free to discuss homework problems and course material with each other, and to consult with the instructor or a TA. Solutions handed in, however, should be the student’s own work.
As a practical guide to applying this policy, you should not look at other student’s solutions or show them your own. Any code that is directly copied (even with trivial modifications) will result in sanctions on both the provider and the recipient. To avoid the appearance of impropriety, it is recommended that if you discuss your assignment with another student, that you take some time performing some unrelated activity before generating your own code, so that you can be confident that what you are doing reflects your own understanding and not someone else’s remembered suggestions.
From time to time, ambiguities and errors may creep into homework assignments. Questions about the interpretation of homework assignments should be sent to the instructor atjames.aspnes@gmail.com. Clarifications will appear in the on-line version of the assignment.
Assignments submitted after the deadline without a Dean’s Extension are automatically assessed a 2%/hour penalty.
There are two purposes to this course: to teach you to program in the C programming language, and to teach you how to choose, implement, and use data structures and standard programming techniques.
On the other hand, there are many reasons why you might not want to use C later in life. It’s missing a lot of features of modern program languages, including:
For most problems where minimizing programmer time and maximizing robustness are more important than minimizing runtime, other languages are a better choice. But for this class, we’ll be using C.
If you want to read a lot of flaming about what C is or is not good for, seehttp://c2.com/cgi/wiki?CeeLanguage.
For small programs, you don’t need much in the way of data structures. But as soon as you are representing reasonably complicated data, you need some place to store it. Thinking about how you want to store and organize this data can be a good framework for organizing the rest of your program.
Many programming environments will give you a rich collection of built-in data structures as part of their standard library. C does not: unless you use third-party libraries, any data structure you want in C you will have to build yourself. For most data structures this will require an understanding of pointers and storage allocation, mechanisms often hidden in other languages. Understanding these concepts will give you a deeper understanding of how computers actually work, and will both let you function in minimalist environments where you don’t have a lot of support and let you understand what more convenient environments are doing under their abstraction barriers.
The same applies to the various programming techniques we will discuss in this class. While some of the issues that come up are specific to C and similar low-level languages (particular issues involving disciplined management of storage), some techniques will apply no matter what kinds of programs you are writing and all will help in understanding what your computer systems are doing even if some of the details are hidden.
The main undergraduate computing facility for Computer Science is the Zoo, located on the third floor of AKW. The Zoo contains a large number of Linux workstations.
You don’t need to do your work for this class in the Zoo, but that is where your assignments will be submitted and tested, so if you do development elsewhere, you will need to copy your files over and make sure that they work there as well.
The best place for information about the Zoo is athttp://zoo.cs.yale.edu/. Below are some points that are of particular relevance for CS223 students.
You should already have an account for the Zoo if you have registered for the class. You login is your NetID. If this doesn’t work, you will need to contact ITS atcs.support@yale.edu to figure out what went wrong.
If you have a Zoo account but do not have a directory under/c/cs223/class (required to submit assignments), create this directory with the commandsudo register cs223.
The Zoo is located on the third floor of Arthur K Watson Hall, toward the front of the building. If you are a Yale student, your ID should get you into the building and the room. If you are not a student, you will need to get your ID validated in AKW 008a to get in after hours.
There are several options for remote use of the Zoo. The simplest is to usessh as described in the following section. This will give you a terminal session, which is enough to run anything you need to if you are not trying to do anything fancy. The related programscp can be used to upload and download files.
The best part of Unix is that nothing ever changes. The instructions below still work, and will get you a terminal window in the Zoo. The only update is that if you are on a Windows machine you can probably now skip using puttySSH and use the built-in ssh client instead.
Date: Mon, 13 Dec 2004 14:34:19 -0500 (EST)From: Jim Faulkner <james.faulkner@yale.edu>Subject: Accessing the ZooHello all,I've been asked to write up a quick guide on how to access the Linux computers in the Zoo. For those who need this information, please read on.There are 2 ways of accessing the Zoo nodes, by walking up to one and logging in on the console (the computers are located on the 3rd floor of AKW), or by connecting remotely via SSH. Telnet access is not allowed. SSH clients for various operating systems are available here:http://www.yale.edu/software/Mac OSX comes with an SSH client by default. A good choice for an SSH client if you run Microsoft Windows is PuTTY:http://www.chiark.greenend.org.uk/~sgtatham/putty/With the exception of a few legacy accounts, the Zoo uses your campus-wide NetID and password for login access. However, you must sign up for a Zooaccount before access is allowed. To sign up for a Zoo account, go to this web page:http://zoo.cs.yale.edu/accounts.htmlThen login with your campus-wide NetID and password. You may choose a different shell, or set up your account to be enrolled in a class if that is appropriate for you, but neither is necessary. Just click "Submit". Within an hour, your Zoo account will be created, and you will receive more information via e-mail about how to access the Zoo.Users cannot log into zoo.cs.yale.edu (the central file server) directly, they must log into one of the Zoo nodes. Following is the list of Zoo nodes:aphid.zoo.cs.yale.edu lion.zoo.cs.yale.edubumblebee.zoo.cs.yale.edu macaw.zoo.cs.yale.educardinal.zoo.cs.yale.edu monkey.zoo.cs.yale.educhameleon.zoo.cs.yale.edu newt.zoo.cs.yale.educicada.zoo.cs.yale.edu peacock.zoo.cs.yale.educobra.zoo.cs.yale.edu perch.zoo.cs.yale.educricket.zoo.cs.yale.edu python.zoo.cs.yale.edufrog.zoo.cs.yale.edu rattlesnake.zoo.cs.yale.edugator.zoo.cs.yale.edu rhino.zoo.cs.yale.edugiraffe.zoo.cs.yale.edu scorpion.zoo.cs.yale.edugrizzly.zoo.cs.yale.edu swan.zoo.cs.yale.eduhare.zoo.cs.yale.edu termite.zoo.cs.yale.eduhippo.zoo.cs.yale.edu tick.zoo.cs.yale.eduhornet.zoo.cs.yale.edu tiger.zoo.cs.yale.edujaguar.zoo.cs.yale.edu tucan.zoo.cs.yale.edukoala.zoo.cs.yale.edu turtle.zoo.cs.yale.eduladybug.zoo.cs.yale.edu viper.zoo.cs.yale.eduleopard.zoo.cs.yale.edu zebra.zoo.cs.yale.eduIf you have already created an account, you can SSH directly to one of the above computers and log in with your campus-wide NetID and password. You can also SSH to node.zoo.cs.yale.edu, which will connect you to a random Zoo node.Feel free to contact me if you have any questions about the Zoo.thanks,Jim FaulknerZoo Systems AdministratorIf for some reason you really want to replicate the full Zoo experience on your own remote machine, you can try running an X server and forwarding your connection.
The first step is to get an X server on your local machine.
- If you are running Linux, you probably already have an X server running.- For OSX, you will probably need to install [XQuartz](https://xquarz.org/).- For Windows, some options are [XMing](http://www.straightrunning.com/XmingNotes/) or XWin-32, which is available through the Yale Software Library.The second step is to use ssh to forward an X connection. Typing “ssh -X netID@node.zoo.cs.yale.edu” to a Linux, OSX, or WSL terminal window should forward an X connection, allowing you to run X clients from the command line on the Zoo and have the windows show up on your local machine. But I’ve only had limited success with this myself, so no promises.
Because C is highly portable, there is a good chance you can develop assignment solutions on your own machine and just upload to the Zoo for final testing and submission. Because there are many different kinds of machines out there, I can only offer very general advice about how to do this.2
You will need a text editor. I likeVim, which will run on pretty much anything, but you should use whatever you are comfortable with.
You will also need a C compiler that can handle at least C99. Ideally you will have an environment that looks enough like Linux that you can also run other command-line tools likegdb,make, and possiblygit. How you get this depends on your underlying OS.
Pretty much any Linux distribution will give you this out of the box. You may need to run your package manager to install missing utilities like thegcc C compiler.
OSX is not Linux, but it is Unix under the hood. You will need a terminal emulator (the built-in Terminal program works, but I likeiTerm2. You will also need to set up XCode to get command-line developer tools. The method for doing this seems to vary depending on which version of XCode you have.
You may end up withgcc being an alias forclang instead of the GNU version ofgcc. Most likely the only difference you will see is the details of the error messages. Remember to test withgcc on the Zoo.
Other packages can be installed using Homebrew. If you are a Mac person you probably already know more about this than I do.
What you can do here depends on your version of Windows.
bash in a console window and get a full-blown Linux installation. You will need to choose a Linux distribution: if you don’t have a preference, I recommend Ubuntu.apt program to install things likegcc. If you use Ubuntu, it will suggest which packages to install if you type a command it doesn’t recognize.ubuntu.exe from Windows to get a nicer terminal emulator than the default console window.See the chapter onhow to use the Zoo for details of particular commands. The basic steps are
vim for long programs andcat for very short ones.)gcc.If any of these steps fail, the next step is debugging. We’ll talk about debugging elsewhere.
Use your favorite text editor. The program file should have a name of the formfoo.c; the.c at the end tells the C compiler the contents are C source code. Here is a typical C program:
#include<stdio.h>/* print the numbers from 1 to 10 */intmain(int argc,char **argv){int i; puts("Now I will count from 1 to 10");for(i =1; i <=10; i++) { printf("%d\n", i); }return0;}Here’s what happens when I compile and run it on the Zoo:
$ gcc -g3 -o count count.c$ ./countNow I will count from 1 to 1012345678910$The first line is the command to compile the program. The dollar sign is myprompt, which is printed by the system to tell me it is waiting for a command. The command callsgcc with arguments-g3 (enable maximum debugging info),-o (specify executable file name, otherwise defaults toa.out),count (the actual executable file name), andcount.c (the source file to compile). This tellsgcc that we should compilecount.c tocount with maximum debugging info included in the executable file.
The second line runs the output filecount. Calling it./count is necessary because by default the shell (the program that interprets what you type) only looks for programs in certain standard system directories. To make it run a program in the current directory, we have to include the directory name.
Noteworthy features of this program include:
#include <stdio.h> in line 1. This is standard C boilerplate, and will appear in any program you see that does input or output. The meaning is to tell the compiler to include the text of the file/usr/include/stdio.h in your program as if you had typed it there yourself. This particular file contains declarations for the standard I/O library functions likeputs (put string) andprintf (print formatted), as used in the program. If you don’t put it in, your program may or may not still compile. Do it anyway./* and*/ characters. Comments are ignored by the compiler but can be helpful for other programmers looking at your code (including yourself, after you’ve forgotten why you wrote something).main function. Every C program has to have amain function declared in exactly this way—it’s what the operating system calls when you execute the program. Theint on Line 3 says that main returns a value of typeint (we’ll describe this in more detail later in the chapter onfunctions), and that it takes two arguments:argc of typeint, the number of arguments passed to the program from the command line, andargv, of apointer type that we will get to eventually, which is an array of the arguments (essentially all the words on the command line, including the program name). Note that it would also work to do this as one line (as K&R typically does); the C compiler doesn’t care about whitespace, so you can format things however you like, subject to the constraint that consistency will make it easier for people to read your code.Everything inside the curly braces is the body of themain function. This includes
int i;, which says thati will be a variable that holds anint (see the chapter onInteger Types).puts (discussed in the chapter oninput and output).for loop on Lines 11–13, which executes its body for each value ofi from 1 to 10. We’ll explain howfor loops worklater. Note that the body of the loop is enclosed in curly braces just like the body of themain function. The only statement in the body is the call toprintf on Line 12; this includes a format string that specifies that we want a decimal-formatted integer followed by a newline (the\n).return 0; on Line 15 tells the operating system that the program worked (the convention in Unix is that 0 means success). If the program didn’t work for some reason, we could have returned something else to signal an error.The Zoo runs a Unix-like operating system called Linux. Most people run Unix with a command-line interface provided by ashell. Each line typed to the shell tells it what program to run (the first word in the line) and what arguments to give it (remaining words). The interpretation of the arguments is up to the program.
When you sign up for an account in the Zoo, you are offered a choice of possible shell programs. The examples below assume you have chosenbash, theBourne-again shell written by the GNU project. Other shells behave similarly for basic commands.
When you log in to a Zoo node directly, you may not automatically get a shell window. If you use the default login environment (which puts you into the KDE window manager), you need to click on the picture of the display with a shell in from of it in the toolbar at the bottom of the screen. If you run Gnome instead (you can change your startup environment using the popup menu in the login box), you can click on the foot in the middle of the toolbar. Either approach will pop up a terminal emulator from which you can run emacs, gcc, and so forth.
The default login shell in the Zoo isbash, and all examples of shell command lines given in these notes will assumebash. You can choose a different login shell on the account sign-up page if you want to, but you are probably best off just learning to likebash.
Most of what one does with Unix programs is manipulate the filesystem. Unix files are unstructured blobs of data whose names are given by paths consisting of a sequence of directory names separated by slashes: for example/home/accts/some-user/cs223/hw1.c. At any time you are in a current working directory (typepwd to find out what it is andcd new-directory to change it). You can specify a file below the current working directory by giving just the last part of the pathname. The special directory names. and.. can also be used to refer to the current directory and its parent. So/home/accts/some-user/cs223/hw1.c is justhw1.c or./hw1.c if your current working directory is/home/accts/some-user/cs223,cs223/hw1.c if your current working directory is/home/accts/some-user, and../cs223/hw1.c if your current working directory is/home/accts/some-user/illegal-downloads.
All Zoo machines share a common filesystem, so any files you create or change on one Zoo machine will show up in the same place on all the others.
Here are some handy Unix commands:
man program will show you the on-line documentation (theman page) for a program (e.g., tryman man orman ls). Handy if you want to know what a program does. On Linux machines like the ones in the Zoo you can also get information usinginfo program, which has an Emacs-like interface.
You can also usemanfunction to see documentation for standard library functions. The commandman -kstring will search for man pages whose titles containstring.
Sometimes there is more than one man page with the same name. In this caseman -k will distingiush them by different manual section numbers, e.g.,printf (1) (a shell command) vs. printf (3) (a library routine). To get a man page from a specific section, usemansectionname, e.g. man 3 printf.
ls lists all the files in the current directory. Some useful variants:
ls /some/other/dir; list files in that directory instead.ls -l; long output format showing modification dates and owners.mkdir dir will create a new directory in the current directory nameddir.rmdir dir deletes a directory. It only works on directories that contain no files.cd dir changes the current working directory. With no arguments,cd changes back to your home directory.pwd (“print working directory”) shows what your current directory is.mv old-name new-name changes the name of a file. You can also use this to move files between directories.cp old-name new-name makes a copy of a file.rm file deletes a file. Deleted files cannot be recovered. Use this command carefully.chmod changes the permissions on a file or directory. See the man page for the full details of how this works. Here are some commonchmod’s:
chmod 644 file; owner can read or write the file, others can only read it.chmod 600 file; owner can read or write the file, others can’t do anything with it.chmod 755 file; owner can read, write, or execute the file, others can read or execute it. This is typically used for programs or for directories (where the execute bit has the special meaning of letting somebody find files in the directory).chmod 700 file; owner can read, write, or execute the file, others can’t do anything with it.emacs,gcc,make,gdb,gitSometimes you may have a running program that won’t die. Aside from costing you the use of your terminal window, this may be annoying to other Zoo users, especially if the process won’t die even if you close the terminal window or log out.
There are various control-key combinations you can type at a terminal window to interrupt or stop a running program.
sigaction system call) will die instantly when you do this. Some won’t.jobs to list all your stopped processes,fg to restart the last process (orfg %1 to start process%1 etc.),bg to keep running the stopped process in the background,kill %1 to attempt to end process%1 politely,kill -KILL %1 to end process%1 even if it is intercepting normal kills.cat > program.c (not really recommmended). For test input, you are often better putting it into a file and using input redirection (./program < test-input-file); this way you can redo the test after you fix the bugs it reveals.If you have a runaway process that you can’t get rid of otherwise, you can useps g to get a list of all your processes and their process ids. Thekill command can then be used on the offending process, e.g. kill -KILL 6666 if your evil process has process id 6666. Sometimes thekillall command can simplify this procedure, e.g. killall -KILL evil halts all process with command nameevil.
If you compile your own program, you will need to prefix it with./ on the command line to tell the shell that you want to run a program in the current directory (called ‘.’) instead of one of the standard system directories. So for example, if I’ve just built a program calledcount, I can run it by typing
$ ./countHere the “$ ” is standing in for whatever your prompt looks like; you should not type it.
Any words after the program name (separated bywhitespace—spaces and/or tabs) are passed in as arguments to the program. Sometimes you may wish to pass more than one word as a single argument. You can do so by wrapping the argument in single quotes, as in
$ ./count 'this is the first argument' 'this is the second argument'Some programs take input fromstandard input (typically the terminal). If you are doing a lot of testing, you will quickly become tired of typing test input at your program. You can tell the shell toredirect standard input from a file by putting the file name after a< symbol, like this:
$ ./count < huge-input-fileA ‘>’ symbol is used to redirectstandard output, in case you don’t want to read it as it flies by on your screen:
$ ./count < huge-input-file > huger-output-fileA useful file for both input and output is the special file/dev/null. As input, it looks like an empty file. As output, it eats any characters sent to it:
$ ./sensory-deprivation-experiment < /dev/null > /dev/nullYou can alsopipe programs together, connecting the output of one to the input of the next. Good programs to put at the end of a pipe arehead (eats all but the first ten lines),tail (eats all but the last ten lines),more (lets you page through the output by hitting the space bar, andtee (shows you the output but also saves a copy to a file). A typical command might be something like./spew | more or./slow-but-boring | tee boring-output. Pipes can consist of a long train of programs, each of which processes the output of the previous one and supplies the input to the next. A typical case might be:
$ ./do-many-experiments | sort | uniq -c | sort -nrwhich, if./do-many-experiments gives the output of one experiment on each line, produces a list of distinct experimental outputs sorted by decreasing frequency. Pipes like this can often substitute for hours of real programming.
To write your programs, you will need to use a text editor, preferably one that knows enough about C to provide tools like automatic indentation and syntax highlighting. There are two reasonable choices for this in the Zoo:emacs, andvim (which can also be run asvi).Emacs andVi have been the two contenders for theOne True Editor since the 1970s—if you learn one (or both) you will be able to use the resulting skills everywhere. My personal preference is to use Vi, but Emacs has the advantage of using the same editing commands as the shell andgdb command-line interfaces.
If you are not editing directly on the Zoo machines, you have more options. Some other popular text editors that work well with C areVS Code,Atom, andSublime Text. The main downside to these editors is that they are relatively new, and so they don’t necessarily show up everywhere the way Emacs or Vi will. The other downside is that their relative novelty means that they won’t necessarily have the same longevity as your grandfather’s text editor: by the time you read this, there is a reasonably good chance that one or more of these popular editors is no longer very popular. But you should feel free to use whatever you are comfortable with as long as it works.
To start Emacs, typeemacs at the command line. If you are actually sitting at a Zoo node it should put up a new window. If not, Emacs will take over the current window. If you have never used Emacs before, you should immediately typeC-h t (this means hold down the Control key, typeh, then typet without holding down the Control key). This will pop you into the Emacs built-in tutorial.
General note:C-x means hold down Control and pressx;M-x means hold down Alt (Emacs calls it “Meta”) and pressx. ForM-x you can also hit Esc and thenx.
C-h t puts up the tutorial,C-h b lists every command available in the current mode,C-h k tells you what a particular sequence of keystrokes does, andC-h l tells you what the last 50 or so characters you typed were (handy if Emacs just garbled your file and you want to know what command to avoid in the future).git (described below).y) for each one, but you can answer no (n) if you changed some file inside Emacs but want to throw the changes away.C-a C-k.M-x compile is that if your program has errors in it, you can typeC-x ` to jump to the next error, or at least wheregcc thinks the next error is.If you don’t find yourself liking Emacs very much, you might want to try Vim instead. Vim is a vastly enhanced reimplementation of the classicvi editor, which I personally find easier to use than Emacs. Typevimtutor to run the tutorial.
One annoying feature of Vim is that it is hard to figure out how to quit. If you don’t mind losing all of your changes, you can always get out by hitting the Escape key a few times and then typing ~~~\\\ :qa!\\\ ~~~
To run Vim, typevim orvim filename from the command line. Or you can use the graphical versiongvim, which pops up its own window.
Vim is amodal editor, meaning that at any time you are in one of several modes (normal mode, insert mode, replace mode, operator-pending mode, etc.), and the interpretation of keystrokes depends on which mode you are in. So typingjjjj in normal mode moves the cursor down four lines, while typingjjjj in insert mode inserts the stringjjjj at the current position. Most of the time you will be in either normal mode or insert mode. There is also a command mode entered by hitting: that lets you type longer commands, similar to the Unix command-line or M-x in Emacs.
a also enters insert mode, but puts new text after the current cursor position instead of before it.R (controlR) will redo the last undo (and can also be repeated).:w filename to write it tofilename. Use:wa to write all files that you have modified. The commandZZ does the same thing without having to hit Enter at the end.:wa for how to fix this, or use:q! if you want to throw away your changes and quit anyway. The shortcuts:x and:wq do a write of the current file followed by quitting.d command. If you precede it with a number, you can delete multiple lines:5dd deletes the next 5 lines. If you replace the secondd with a motion command, you delete until wherever you land:d$ deletes to end of line (D is faster),dj deletes this line and the line after it,d% deletes the next matching group of parentheses/braces/brackets and whatever is between them,dG deletes to end of file—there are many possibilities. All of these save what you deleted into register"" so you can get them back withp.dd, but only saves the line to register"" and doesn’t delete it. (Thinkcopy). All the variants ofdd work withyy:5yy,y$,yj,y%, etc."". (Thinkpaste).make in the current directory. You can also give it arguments, e.g.,:make myprog,:make test. Use:cn to go to the next error if you get errors.:! echo hello world or:! gdb myprogram. Returns to Vim when the command exits (control-C can sometimes be helpful if your command isn’t exiting when it should). This works best if you ran Vim from a shell window; it doesn’t work very well if Vim is running in its own window.informativeVariableName instead ofivn, you can avoid having to type the entire word by typinginf<control-P> if it’s the only word in your buffer that starts withinf.Unlike Emacs, Vim’s default settings are not very good for editing C programs. You can fix this by creating a file called.vimrc in your home directory with the following commands:
set shiftwidth=4set autoindentset backupset cindentset hlsearchset incsearchset showmatchset numbersyntax onfiletype plugin onfiletype indent onexamples/sample.vimrc(You can download this file by clicking on the link.)
In Vim, you can type e.g. :help backup to find out what each setting does. Note that because.vimrc starts with a., it won’t be visible tols unless you usels -a orls -A.
gccA C program will typically consist of one or more files whose names end with.c. To compilefoo.c, you can typegcc foo.c. Assumingfoo.c contains no errors egregious enough to be detected by the extremely forgiving C compiler, this will produce a file nameda.out that you can then execute by typing./a.out.
If you want to debug your program usinggdb or give it a different name, you will need to use a longer command line. Here’s one that compilesfoo.c tofoo (run it using./foo) and includes the information thatgdb needs:gcc -g3 -o foo foo.c
By default,gcc doesn’t check everything that might be wrong with your program. But if you give it a few extra arguments, it will warn you about many (but not all) potential problems:gcc -g3 -Wall -o foo foo.c.
For complicated programs involving multiple source files, you are probably better off usingmake than callinggcc directly. Make is a “rule-based expert system” that figures out how to compile programs given a little bit of information about their components.
For example, if you have a file calledfoo.c, try typingmake foo and see what happens.
In general you will probably want to write aMakefile, which is namedMakefile ormakefile and tellsmake how to compile programs in the same directory. Here’s a typical Makefile:
# Any line that starts with a sharp is a comment and is ignored# by Make.# These lines set variables that control make's default rules.# We STRONGLY recommend putting "-g3 -Wall" in your CFLAGS.CC=gccCFLAGS=-g3 -Wall# The next line is a dependency line.# It says that if somebody types "make all"# make must first make "hello-world".# By default the left-hand-side of the first dependency is what you# get if you just type "make" with no arguments.all: hello-world# How do we make hello-world?# The dependency line says you need to first make hello-world.o# and hello-library.ohello-world: hello-world.o hello-library.o# Subsequent lines starting with a TAB character give# commands to execute.# This command uses make built-in variables to avoid# retyping (and getting things wrong):# $@ = target hello-world# $^ = dependencies hello-world.o and hello-library.o$(CC)$(CFLAGS) -o$@$^# You can put whatever commands you want.echo"I just built hello-world! Hooray!"# Here we are saying that hello-world.o and hello-library.o# should be rebuilt whenever their corresponding source file# or hello-library.h changes.# There are no commands attached to these dependency lines, so# make will have to figure out how to do that somewhere else# (probably from the builtin .c -> .o rule).hello-world.o: hello-world.c hello-library.hhello-library.o: hello-library.c hello-library.h# Command lines can do more than just build things. For example,# "make test" will rebuild hello-world (if necessary) and then run it.test: hello-world./hello-world# This lets you type "make clean" and get rid of anything you can# rebuild. The $(RM) variable is predefined to "rm -f"clean:$(RM) hello-world *.oGiven a Makefile, make looks at each dependency line and asks: (a) does the target on the left hand side exist, and (b) is it older than the files it depends on. If so, it looks for a set of commands for rebuilding the target, after first rebuilding any of the files it depends on; the commands it runs will be underneath some dependency line where the target appears on the left-hand side. It has built-in rules for doing common tasks like building.o files (which contain machine code) from.c files (which contain C source code). If you have a fake target likeall above, it will try to rebuild everythingall depends on because there is no file namedall (one hopes).
Make really really cares that the command lines start with a TAB character. TAB looks like eight spaces in Emacs and other editors, but it isn’t the same thing. If you put eight spaces in (or a space and a TAB), Make will get horribly confused and give you an incomprehensible error message about a “missing separator”. This misfeature is so scary that I avoided using make for years because I didn’t understand what was going on. Don’t fall into that trap—make really is good for you, especially if you ever need to recompile a huge program when only a few source files have changed.
If you use GNU Make (on a zoo node), note that beginning with version 3.78, GNU Make prints a message that hints at a possible SPACEs-vs-TAB problem, like this:
$ makeMakefile:23:*** missing separator (did you mean TAB instead of 8 spaces?). Stop.If you need to repair a Makefile that uses spaces, one way of converting leading spaces into TABs is to use theunexpand program:
$ mv Makefile Makefile.old$ unexpand Makefile.old > MakefileThe standard debugger on the Zoo isgdb. Also useful is the memory error checkervalgrind. Below are some notes on debugging in general and using these programs in particular.
Basic method of all debugging:
A tempting mistake is to skip step 1, and just try randomly tweaking things until the program works. Better is to see what the program is doing internally, so you can see exactly where and when it is going wrong. A second temptation is to attempt to intuit where things are going wrong by staring at the code or the program’s output. Avoid this temptation as well: let the computer tell you what it is really doing inside your program instead of guessing.
Every non-trivial C program should include<assert.h>, which gives you theassert macro (see Appendix B6 of K&R). Theassert macro tests if a condition is true and halts your program with an error message if it isn’t:
Compiling and running this program produces the following output:
$ gcc -o no no.c$ ./nono: no.c:6: main: Assertion `2+2 == 5' failed.Line numbers and everything, even if you compile with the optimizer turned on. Much nicer than a mere segmentation fault, and if you run it under the debugger, the debugger will stop exactly on the line where theassert failed so you can poke around and see why.
gdbThe standard debugger on Linux is calledgdb. This lets you run your program under remote control, so that you can stop it and see what is going on inside.
You can also useddd, which is a graphical front-end forgdb. There is anextensive tutorial available forddd, so we will concentrate on the command-line interface togdb here.
Warning: Thoughgdb is rock-solid when running on an actual Linux kernel, if you are running on a different underlying operating system like Windows (including Windows Subsystem for Linux) or OS X, it may not work as well, either missing errors that it should catch or in some cases not starting at all. In either case you can try debugging on the Zoo machines instead. For OS X, you might also have better results using the standard OS X debuggerlldb, which is similar enough togdb to do everythinggdb can do while being different enough that you will need to learn its own set of commands. Most IDEs that support C also include debugging tools.
Getting back togdb, we’ll look at a contrived example. Suppose you have the following programbogus.c:
#include<stdio.h>/* Print the sum of the integers from 1 to 1000 */intmain(int argc,char **argv){int i;int sum; sum =0;for(i =0; i -=1000; i++) { sum += i; } printf("%d\n", sum);return0;}Let’s compile and run it and see what happens. Note that we include the flag-g3 to tell the compiler to include debugging information. This allowsgdb to translate machine addresses back into identifiers and line numbers in the original program for us.
$ gcc -g3 -o bogus bogus.c$ ./bogus-34394132$That doesn’t look like the sum of 1 to 1000. So what went wrong? If we were clever, we might notice that the test in the for loop is using the mysterious-= operator instead of the<= operator that we probably want. But let’s suppose we’re not so clever right now—it’s four in the morning, we’ve been working onbogus.c for twenty-nine straight hours, and there’s a-= up there because in our befuddled condition we know in our bones that it’s the right operator to use. We need somebody else to tell us that we are deluding ourselves, but nobody is around this time of night. So we’ll have to see what we can get the computer to tell us.
The first thing to do is fire upgdb, the debugger. This runs our program in stop-motion, letting us step through it a piece at a time and watch what it is actually doing. In the example below gdb is run from the command line. You can also run it directly from Emacs withM-x gdb, which lets Emacs track and show you where your program is in the source file with a little arrow, or (if you are logged in directly on a Zoo machine) by runningddd, which wrapsgdb in a graphical user interface.
$ gdb bogusGNU gdb 4.17.0.4 with Linux/x86 hardware watchpoint and FPU supportCopyright 1998 Free Software Foundation, Inc.GDB is free software, covered by the GNU General Public License, and you arewelcome to change it and/or distribute copies of it under certain conditions.Type "show copying" to see the conditions.There is absolutely no warranty for GDB. Type "show warranty" for details.This GDB was configured as "i386-redhat-linux"...(gdb) runStarting program: /home/accts/aspnes/tmp/bogus -34394132Program exited normally.So far we haven’t learned anything. To see our program in action, we need to slow it down a bit. We’ll stop it as soon as it entersmain, and step through it one line at a time while having it print out the values of the variables.
(gdb) break mainBreakpoint 1 at 0x8048476: file bogus.c, line 9.(gdb) runStarting program: /home/accts/aspnes/tmp/bogus Breakpoint 1, main (argc=1, argv=0xbffff9ac) at bogus.c:99 sum = 0;(gdb) display sum1: sum = 1(gdb) n10 for(i = 0; i -= 1000; i++)1: sum = 0(gdb) display i2: i = 0(gdb) n11 sum += i;2: i = -10001: sum = 0(gdb) n10 for(i = 0; i -= 1000; i++)2: i = -10001: sum = -1000(gdb) n11 sum += i;2: i = -19991: sum = -1000(gdb) n10 for(i = 0; i -= 1000; i++)2: i = -19991: sum = -2999(gdb) quitThe program is running. Exit anyway? (y or n) y$Here we are usingbreak main to tell the program to stop as soon as it entersmain,display to tell it to show us the value of the variablesi andsum whenever it stops, andn (short fornext) to execute the program one line at a time.
When stepping through a program, gdb displays the line it will executenext as well as any variables you’ve told it to display. This means that any changes you see in the variables are the result of theprevious displayed line. Bearing this in mind, we see thati drops from 0 to -1000 the very first time we hit the top of thefor loop and drops to -1999 the next time. So something bad is happening in the top of thatfor loop, and if we squint at it a while we might begin to suspect thati -= 1000 is not the nice simple test we might have hoped it was.
break somefunction stops before executing the first linesomefunction. -break 117 stops before executing line number 117.list somefunc lists all lines ofsomefunc. -list 117-123 lists lines 117 through 123.continue). Continue until (a) the end of the program, (b) a fatal error like a Segmentation Fault or Bus Error, or (c) a breakpoint. If you give it a numeric argument (e.g.,cont 1000) it will skip over that many breakpoints before stopping.print i.print, but runs automatically every time the program stops. Useful for watching values that change often.bt. Dobt full if you also want to see local variables in each function.gdb but not inside. Normally the Linux kernel randomizes the position of bits of your program before running it, to make its response to buffer overflow attacks less predictable. By default,gdb turns this off so that the behavior of your program is consistent from one execution to the next. But sometimes this means that a pointer that had been bad with address randomization (causing a segmentation fault) turns out not to be bad without. This option will restore the standard behavior even insidegdb and give you some hope of finding what went wrong.In general, the idea behind debugging is that a bad program starts out OK, but after executing for a while it gets confused and starts misbehaving. If you can find the exact moment in its execution where it first starts acting up, you can see exactly what piece of code is causing the problem and have a reasonably good chance of being able to fix it. So a typical debugging strategy is to put in a breakpoint (usingbreak) somewhere before the confusion hits, “instrument” the program (usingdisplay) so that you can watch it getting confused, and step through it (usingnext,step, or breakpoints andcont) until you find the point of failure. Sometimes this process requires restarting the program (usingrun) if you skip over this point without noticing it immediately.
For large or long-running programs, it often makes sense to do binary search to find the point of failure. Put in a breakpoint somewhere (say, on a function that is called many times or at the top of a major loop) and see what the state of the program is after going through the breakpoint 1000 times (using something likecont 1000). If it hasn’t gone off the rails yet, try restarting and going through 2000 times. Eventually you bracket the error as occurring (for example) somewhere between the 4000th and 8000th occurrence of the breakpoint. Now try stepping through 6000 times; if the program is looking good, you know the error occurs somewhere between the 6000th and 8000th breakpoint. Adding a dozen or so more experiments like this should be enough isolate the bug to a specific line of code.
The key to all debugging is knowing what your code is supposed to do. If you don’t know this, then you can’t tell the cases where it is working from the cases where it isn’t. If you’re confused about what your code is supposed to be doing, you need to figure out what exactly you want it to do. If you can figure that out, often it will be obvious what is going wrong. If it isn’t obvious, you can always go back togdb.
gdbHere are some typical classes of bugs and how to squish them withgdb. (The same instructions usually work forddd.)
-g3 flag. You can still rungdb if you don’t do this, but it won’t be able to show you variable names or source lines.gdb withgdbprogramname.break main to stop at the start of themain routine.runarguments. Therun command stands in for the program name. You can also redirect input as in the shell withrunarguments <filename.display, as indisplay x,display a[i],display z+17. Inddd, double-clicking on a variable name will have the same effect. Useundisplay to get rid of any displays you don’t want.next (always goes to next line in the current function, not dropping down into function calls),step (go to the next executed line, even if it is inside a called function),finish (run until the current function returns), andcont (run until the end of the program or the next breakpoint).This can be handy if you don’t particularly know what is going on in your program and want to see.
Run the program as described above. When you hit the badassert, you will stop several functions deep from where it actually happened. Useup to get up to the function that has the call toassert then useprint ordisplay to figure out what is going on.
Example program:
#include<stdio.h>#include<stdlib.h>#include<assert.h>intmain(int argc,char **argv){int x; x =3; assert(x+x ==4);return0;}Withgdb in action:
$ gcc -g3 -o assertFailed assertFailed.c $ gdb assertFailedGNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1Copyright (C) 2014 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law. Type "show copying"and "show warranty" for details.This GDB was configured as "i686-linux-gnu".Type "show configuration" for configuration details.For bug reporting instructions, please see:<http://www.gnu.org/software/gdb/bugs/>.Find the GDB manual and other documentation resources online at:<http://www.gnu.org/software/gdb/documentation/>.For help, type "help".Type "apropos word" to search for commands related to "word"...Reading symbols from assertFailed...done.(gdb) runStarting program: /home/aspnes/g/classes/223/notes/examples/debugging/assertFailed assertFailed: assertFailed.c:12: main: Assertion `x+x == 4' failed.Program received signal SIGABRT, Aborted.0xb7fdd416 in __kernel_vsyscall ()(gdb) up#1 0xb7e43577 in __GI_raise (sig=sig@entry=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:5656 ../nptl/sysdeps/unix/sysv/linux/raise.c: No such file or directory.(gdb) up#2 0xb7e469a3 in __GI_abort () at abort.c:8989 abort.c: No such file or directory.(gdb) up#3 0xb7e3c6c7 in __assert_fail_base (fmt=0xb7f7a8b4 "%s%s%s:%u: %s%sAssertion `%s' failed.\n%n", assertion=assertion@entry=0x804850f "x+x == 4", file=file@entry=0x8048500 "assertFailed.c", line=line@entry=12, function=function@entry=0x8048518 <__PRETTY_FUNCTION__.2355> "main") at assert.c:9292 assert.c: No such file or directory.(gdb) up#4 0xb7e3c777 in __GI___assert_fail (assertion=0x804850f "x+x == 4", file=0x8048500 "assertFailed.c", line=12, function=0x8048518 <__PRETTY_FUNCTION__.2355> "main") at assert.c:101101 in assert.c(gdb) up#5 0x0804845d in main (argc=1, argv=0xbffff434) at assertFailed.c:1212 assert(x+x == 4);(gdb) print x$1 = 3Here we see thatx has value 3, which may or may not be the right value, but certainly violates the assertion.
Very much like the previous case. Rungdb until the segmentation fault hits, then look around for something wrong.
#include<stdio.h>#include<stdlib.h>#include<assert.h>intmain(int argc,char **argv){int a[1000];int i; i = -1771724; printf("%d\n", a[i]);return0;}$ gcc -g3 -o segmentationFault segmentationFault.c $ gdb segmentationFaultGNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1[...]Reading symbols from segmentationFault...done.(gdb) runStarting program: /home/aspnes/g/classes/223/notes/examples/debugging/segmentationFault Program received signal SIGSEGV, Segmentation fault.0x08048435 in main (argc=1, argv=0xbffff434) at segmentationFault.c:1313 printf("%d\n", a[i]);(gdb) print a[i]$1 = 0(gdb) print i$2 = -1771724Curiously,gdb has no problem coming up with a value fora[i]. Buti looks pretty suspicious.
Rungdb, wait a while, then hit control-C. This will stopgdb wherever it is. If you have an infinite loop, it’s likely that you will be in it, and that the index variables will be doing something surprising. Usedisplay to keep an eye on them and donext a few times.
$ gcc -g3 -o infiniteLoop infiniteLoop.c$ gdb infiniteLoopGNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1[...]Reading symbols from infiniteLoop...done.(gdb) runStarting program: /home/aspnes/g/classes/223/notes/examples/debugging/infiniteLoop ^CProgram received signal SIGINT, Interrupt.main (argc=1, argv=0xbffff434) at infiniteLoop.c:1111 i *= 37;(gdb) display i1: i = 0(gdb) n10 for(i = 0; i < 10; i += 0) {1: i = 0(gdb) n11 i *= 37;1: i = 0(gdb) n10 for(i = 0; i < 10; i += 0) {1: i = 0(gdb) n11 i *= 37;1: i = 0(gdb) n10 for(i = 0; i < 10; i += 0) {1: i = 0(gdb) n11 i *= 37;1: i = 0Sometimes pointer botches don’t manifest as good, honest segmentation faults but instead as mysterious changes to seemingly unrelated variables. You can catch these in the act using conditional breakpoints. The downside is that you can only put conditional breakpoints on particular lines.
Here’s a program that violates array bounds (which C doesn’t detect):
#include<stdio.h>#include<stdlib.h>#include<assert.h>intmain(int argc,char **argv){int x;int a[10];int i; x =5;for(i = -1; i <11; i++) { a[i] =37; } assert(x ==5);return0;}In the debugging session below, it takes a couple of attempts to catch the change inx before hitting the failed assertion.
$ gcc -g3 -o mysteryChange mysteryChange.c $ gdb mysteryChangeGNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1[...]Reading symbols from mysteryChange...done.(gdb) runStarting program: /home/aspnes/g/classes/223/notes/examples/debugging/mysteryChange mysteryChange: mysteryChange.c:18: main: Assertion `x == 5' failed.Program received signal SIGABRT, Aborted.0xb7fdd416 in __kernel_vsyscall ()(gdb) list main2 #include <stdlib.h>3 #include <assert.h>4 5 int6 main(int argc, char **argv)7 {8 int x;9 int a[10];10 int i;11 (gdb) list12 x = 5;13 14 for(i = -1; i < 11; i++) {15 a[i] = 37;16 }17 18 assert(x == 5);19 20 return 0;21 }(gdb) break 14 if x != 5Breakpoint 1 at 0x804842e: file mysteryChange.c, line 14.(gdb) runThe program being debugged has been started already.Start it from the beginning? (y or n) yStarting program: /home/aspnes/g/classes/223/notes/examples/debugging/mysteryChange mysteryChange: mysteryChange.c:18: main: Assertion `x == 5' failed.Program received signal SIGABRT, Aborted.0xb7fdd416 in __kernel_vsyscall ()(gdb) break 15 if x != 5Breakpoint 2 at 0x8048438: file mysteryChange.c, line 15.(gdb) runThe program being debugged has been started already.Start it from the beginning? (y or n) yStarting program: /home/aspnes/g/classes/223/notes/examples/debugging/mysteryChange Breakpoint 2, main (argc=1, argv=0xbffff434) at mysteryChange.c:1515 a[i] = 37;(gdb) print i$1 = 0(gdb) print a[0]$2 = 134520832(gdb) print a[-1]$3 = 37(gdb) print x$4 = 37One thing to note is that a breakpoint stops before the line it is on executes. So when we hit the breakpoint on line 15 (gdb having observed thatx != 5 is true),i has the value 0, but the damage happened in the previous interation wheni was -1. If we want to see exactly what happened then, we’d need to go back in time. We can’t do this, but we could set an earlier breakpoint and run the program again.
Thevalgrind program can be used to detect some (but not all) common errors in C programs that use pointers and dynamic storage allocation. On the Zoo, you can runvalgrind on your program by puttingvalgrind at the start of the command line:
valgrind ./my-program arg1 arg2 < test-inputThis will run your program and produce a report of any allocations and de-allocations it did. It will also warn you about common errors like using unitialized memory, dereferencing pointers to strange places, writing off the end of blocks allocated usingmalloc, or failing to free blocks.
You can suppress all of the output except errors using the-q option, like this:
valgrind -q ./my-program arg1 arg2 < test-inputYou can also turn on more tests, e.g.
valgrind -q --tool=memcheck --leak-check=yes ./my-program arg1 arg2 < test-inputSeevalgrind --help for more information about the (many) options, or look at the documentation athttp://valgrind.org/ for detailed information about what the output means. For some commonvalgrind messages, see the examples section below.
If you want to runvalgrind on your own machine, you may be able to find a version that works athttp://valgrind.org. Unfortunately, this is only likely to work if you are running a Unix-like operating system. This does include Linux (either on its own or inside Windows Subsystem for Linux) and OSX, but it does not include stock Windows.
You can runvalgrind on any program (tryvalgrind ls); it does not require special compilation. However, the output ofvalgrind will be more informative if you compile your program with debugging information turned on using the-g or-g3 flags (this is also useful if you plan to watch your program running usinggdb, ).
Unless otherwise specified, automated testing of your program will be done using the script in/c/cs223/bin/vg; this runs/c/cs223/bin/valgrind with the--tool=memcheck,--leak-check=yes, and-q options, throws away your program’s output, and replaces it withvalgrind’s output. If you have a program named./prog, running/c/cs223/bin/vg ./prog should produce no output.
Here are some examples ofvalgrind output. In each case the example program is compiled with-g3 so thatvalgrind can report line numbers from the source code.
You may also find it helpful to play withthis demo program written by the Spring 2018 course staff.
Consider this unfortunate program, which attempts to compare two strings, one of which we forgot to ensure was null-terminated:
#include<stdio.h>intmain(int argc,char **argv){char a[2]; a[0] ='a';if(!strcmp(a,"a")) { puts("a is\"a\""); }return0;}Run without valgrind, we see no errors, because we got lucky and it turned out our hand-built string was null-terminated anyway:
$ ./uninitialized a is "a"Butvalgrind is not fooled:
$ valgrind -q ./uninitialized ==4745== Conditional jump or move depends on uninitialised value(s)==4745== at 0x4026663: strcmp (mc_replace_strmem.c:426)==4745== by 0x8048435: main (uninitialized.c:10)==4745== ==4745== Conditional jump or move depends on uninitialised value(s)==4745== at 0x402666C: strcmp (mc_replace_strmem.c:426)==4745== by 0x8048435: main (uninitialized.c:10)==4745== ==4745== Conditional jump or move depends on uninitialised value(s)==4745== at 0x8048438: main (uninitialized.c:10)==4745==Here we get a lot of errors, but they are all complaining about the same call tostrcmp. Since it’s unlikely thatstrcmp itself is buggy, we have to assume that we passed some uninitialized location into it that it is looking at. The fix is to add an assignmenta[1] = '\0' so that no such location exists.
Here is a program that callsmalloc but notfree:
With no extra arguments,valgrind will not look for this error. But if we turn on--leak-check=yes, it will complain:
$ valgrind -q --leak-check=yes ./missing_free==4776== 26 bytes in 1 blocks are definitely lost in loss record 1 of 1==4776== at 0x4024F20: malloc (vg_replace_malloc.c:236)==4776== by 0x80483F8: main (missing_free.c:9)==4776==Here the stack trace in the output shows where the bad block was allocated: insidemalloc (specifically the paranoid replacementmalloc supplied byvalgrind), which was in turn called bymain in line 9 ofmissing_free.c. This lets us go back and look at what block was allocated in that line and try to trace forward to see why it wasn’t freed. Sometimes this is as simple as forgetting to include afree statement anywhere, but in more complicated cases it may be because I somehow lose the pointer to the block by overwriting the last variable that points to it or by embedding it in some larger structure whose components I forget to free individually.
These are usually operations that you do off the end of a block frommalloc or on a block that has already been freed.
An example of the first case:
#include<stdio.h>#include<stdlib.h>#include<assert.h>intmain(int argc,char **argv){char *s; s = malloc(1); s[0] ='a'; s[1] = '\0'; puts(s);return0;}==7141== Invalid write of size 1==7141== at 0x804843B: main (invalid_operations.c:12)==7141== Address 0x419a029 is 0 bytes after a block of size 1 alloc'd==7141== at 0x4024F20: malloc (vg_replace_malloc.c:236)==7141== by 0x8048428: main (invalid_operations.c:10)==7141== ==7141== Invalid read of size 1==7141== at 0x4026063: __GI_strlen (mc_replace_strmem.c:284)==7141== by 0x409BCE4: puts (ioputs.c:37)==7141== by 0x8048449: main (invalid_operations.c:14)==7141== Address 0x419a029 is 0 bytes after a block of size 1 alloc'd==7141== at 0x4024F20: malloc (vg_replace_malloc.c:236)==7141== by 0x8048428: main (invalid_operations.c:10)==7141==An example of the second:
#include<stdio.h>#include<stdlib.h>#include<assert.h>intmain(int argc,char **argv){char *s; s = malloc(2); free(s); s[0] ='a'; s[1] = '\0'; puts(s);return0;}==7144== Invalid write of size 1==7144== at 0x804846D: main (freed_block.c:13)==7144== Address 0x419a028 is 0 bytes inside a block of size 2 free'd==7144== at 0x4024B3A: free (vg_replace_malloc.c:366)==7144== by 0x8048468: main (freed_block.c:11)==7144== ==7144== Invalid write of size 1==7144== at 0x8048477: main (freed_block.c:14)==7144== Address 0x419a029 is 1 bytes inside a block of size 2 free'd==7144== at 0x4024B3A: free (vg_replace_malloc.c:366)==7144== by 0x8048468: main (freed_block.c:11)==7144== ==7144== Invalid read of size 1==7144== at 0x4026058: __GI_strlen (mc_replace_strmem.c:284)==7144== by 0x409BCE4: puts (ioputs.c:37)==7144== by 0x8048485: main (freed_block.c:16)[... more lines of errors deleted ...]In both cases the problem is that we are operating on memory that is not guaranteed to be allocated to us. For short programs like these, we might get lucky and have the program work anyway. But we still want to avoid bugs like this because we might not get lucky.
How do we know which case is which? If I write off the end of an existing block, I’ll see something likeAddress 0x419a029 is 0 bytes after a block of size 1 alloc'd, telling me that I am working on an address after a block that is still allocated. When I try to write to a freed block, the message changes toAddress 0x419a029 is 1 bytes inside a block of size 2 free'd, where thefree'd part tells me I freed something I probably shouldn’t have. Fixing the first class of bugs is usually just a matter of allocating a bigger block (but don’t just do this without figuring outwhy you need a bigger block, or you’ll just be introducing random mutations into your code that may cause other problems elsewhere). Fixing the second class of bugs usually involves figuring out why you freed this block prematurely. In some cases you may need to re-order what you are doing so that you don’t free a block until you are completely done with it.
A tempting but usually bad approach to debugging is to put lots ofprintf statements in your code to show what is going on. The problem with this compared to usingassert is that there is no built-in test to see if the output is actually what you’d expect. The problem compared togdb is that it’s not flexible: you can’t change your mind about what is getting printed out without editing the code. A third problem is that the output can be misleading: in particular,printf output is usually buffered, which means that if your program dies suddenly there may be output still in the buffer that is never flushed tostdout. This can be very confusing, and can lead you to believe that your program fails earlier than it actually does.
If you really need to useprintf or something like it for debugging output, here are a few rules of thumb to follow to mitigate the worst effects:
fprintf(stderr, ...) instead ofprintf(...); this allows you to redirect your program’s regular output somewhere that keeps it separate from the debugging output (but beware of misleading interleaving of the two streams—buffering may mean that output tostdout andstderr appears to arrive out of order). It also helps that output tostderr is usually unbuffered, avoiding the problem of lost output.stdout, putfflush(stdout) after any output operation you suspect is getting lost in the buffer. Thefflush function forces any buffered output to be emitted immediately.printf as simple as possible and beware of faults in your debugging code itself. If you writeprintf("a[key] == %d\n", a[key]) andkey is some bizarre value, you will never see the result of thisprintf because your program will segfault while evaluatinga[key]. Naturally, this is more likely to occur if the argument isa[key]->size[LEFTOVERS].cleanupFunction(a[key]) than if it’s justa[key], and if it happens it will be harder to figure out where in this complex chain of array indexing and pointer dereferencing the disaster happened. Better is to wait for your program to break ingdb, and use theprint statement on increasingly large fragments of the offending expression to see where the bogus array index or surprising null pointer is hiding.#ifdef so you can turn it on and off easily.Bearing in mind that this is a bad idea, here is an example of how one might do it as well as possible:
#include<stdio.h>#include<stdlib.h>#include<assert.h>/* initialize the application */voidinit(void){int x; x = *((int *)0xbad1dea);/* if we are lucky, maybe the optimizer will remove it? */}intmain(int argc,char **argv){ init();#ifdef DEBUGGING_OUTPUT/* * this type of debugging output is not recommended * but if you do it anyway: * * 1. Use stderr, which flushes automatically. * 2. Be wary of buffered data on stdout. * 3. Wrap your debugging statement in an #ifdef, * so it is not active by default. */ fputs("Returned from init() in main()\n", stderr);#endifreturn0;}Note that we get much more useful information if we run this undergdb (which will stop exactly on the bad line ininit), but not seeing the result of thefputs at least tells us something.
Chapter 7 of Kernighan and Pike,The Practice of Programming (Addison-Wesley, 1998) gives an excellent overview of performance tuning. This page will be limited to some Linux-specific details and an example.
Usetime, e.g.
$ time wc /usr/share/dict/words 45378 45378 408865 /usr/share/dict/wordsreal 0m0.010suser 0m0.006ssys 0m0.004sThis measures “real time” (what it sounds like), “user time” (the amount of time the program runs), and “system time” (the amount of time the operating system spends supporting your program, e.g. by loading it from disk and doing I/O). Real time need not be equal to the sum of user time and system time, since the operating system may be simultaneously running other programs.
Particularly for fast programs, times can vary from one execution to the next, e.g.
$ time wc /usr/share/dict/words 45378 45378 408865 /usr/share/dict/wordsreal 0m0.009suser 0m0.008ssys 0m0.001s$ time wc /usr/share/dict/words 45378 45378 408865 /usr/share/dict/wordsreal 0m0.009suser 0m0.007ssys 0m0.002sThis arises because of measurement errors and variation in how long different operations take. But usually the variation will not be much.
Note also thattime is often a builtin operation of your shell, so the output format may vary depending on what shell you use.
valgrindThe problem withtime is that it only tells you how much time your whole program took, but not where it spent its time. This is similar to looking at a program without a debugger: you can’t see what’s happening inside. If you want to see where your program is spending its time, you need to use aprofiler.
The specific profiler we will use in this section iscallgrind, a tool built intovalgrind, which we’ve been using elsewhere to detect pointer disasters and storage leaks. Full documentation forcallgrind can be found athttp://valgrind.org/docs/manual/cl-manual.html, but we’ll give an example of typical use here.
Here is an example of a program that is unreasonably slow for what it is doing.
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<string.h>/* concatenate n copies of src into dest *//* returns dest *//* caller is responsible for making dest large enough */char *replicate(char *dest,constchar *src,int n){/* truncate dest */ dest[0] = '\0';/* BAD: each call to strcat requires walking across dest */for(int i =0; i < n; i++) { strcat(dest, src); }return dest;}/* like strcpy, but only copies characters at indices 0, 2, 4, ... from src to dest */char *copyEvenCharacters(char *dest,constchar *src){int i;int j;/* BAD: Calls strlen on every pass through the loop */for(i =0, j =0; i < strlen(src); i +=2, j++) { dest[j] = src[i]; } dest[j] = '\0';return dest;}/* like puts, but stops after maxChars characters */voidputsRestricted(constchar *s,int maxChars){for(int i =0; s[i]; i++) {if(i >= maxChars) { printf("[%zu more]\n", strlen(s) - i);return; }else { putchar(s[i]); } } putchar('\n');}#define SMALL (10)#define BIG (100000)#define PATTERN "abcd"#define BUFFER_SIZE (BIG * strlen(PATTERN) + 1)/* how many characters to print at once */#define MAX_CHARS (40)intmain(int argc,char **argv){char *buffer;char *half; buffer = malloc(BUFFER_SIZE); half = malloc(BUFFER_SIZE); putsRestricted(replicate(buffer, PATTERN, SMALL), MAX_CHARS); putsRestricted(copyEvenCharacters(half, buffer), MAX_CHARS); putsRestricted(replicate(buffer, PATTERN, BIG), MAX_CHARS); putsRestricted(copyEvenCharacters(half, buffer), MAX_CHARS); free(half); free(buffer);return0;}This program defines several functions for processing null-terminated strings:replicate, which concatenates many copies of some string together, andcopyEvenCharacters, which copies every other character in a string to a given buffer. Unfortunately, both functions contain a hidden inefficiency arising from their use of the standard C library string functions.
The runtime of the program is not terrible, but not as sprightly as we might expect given that we are working on less than half a megabyte of text:
$ time ./slowabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdacacacacacacacacacacabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd[399960 more]acacacacacacacacacacacacacacacacacacacac[199960 more]real 0m3.171suser 0m3.164ssys 0m0.001sSo we’d like to make it faster.
In this particular case, the programmer was kind enough to identify the problems in the original code in comments, but we can’t always count on that. But we can use thecallgrind tool built intovalgrind to find out where our program is spending most of its time.
To runcallgrind, callvalgrind with the--tool=callgrind option, like this:
$ time valgrind --tool=callgrind ./slow==5714== Callgrind, a call-graph generating cache profiler==5714== Copyright (C) 2002-2017, and GNU GPL'd, by Josef Weidendorfer et al.==5714== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info==5714== Command: ./slow==5714== ==5714== For interactive control, run 'callgrind_control -h'.abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdacacacacacacacacacacabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd[399960 more]acacacacacacacacacacacacacacacacacacacac[199960 more]==5714== ==5714== Events : Ir==5714== Collected : 15339385208==5714== ==5714== I refs: 15,339,385,208real 1m31.965suser 1m31.515ssys 0m0.037sI’ve includetime at the start of the command line to make it clear just how much of a slowdown you can expect from usingvalgrind for this purpose. Note thatvalgrind only prints a bit of summary data while executing. To get a full report, we use a separate programcallgrind_annotate:
$ callgrind_annotate --auto=yes --inclusive=yes > slow.callgrindHere I sent the output to a fileslow.callgrind so I could look at it in more detail in my favorite text editor, since the actual report is pretty huge. The--auto=yes argument tellscallgrind_annotate to show how many instructions were executed as part of each line of source code, and the--inclusive=yes argument tells use that in its report it should charge instructions executed in some function both to that function and to all functions responsible for calling it. This is usually what you want to figure out where things are going wrong.
The first thing to look at inslow.callgrind is the table showing which functions are doing most of the work:
-------------------------------------------------------------------------------- Ir file:function--------------------------------------------------------------------------------15,339,385,208 ???:0x0000000000000dd0 [/usr/lib64/ld-2.25.so]15,339,274,304 ???:_start [/home/accts/aspnes/g/classes/223/notes/examples/profiling/slow]15,339,274,293 /usr/src/debug/glibc-2.25-123-gedcf13e25c/csu/../csu/libc-start.c:(below main) [/usr/lib64/libc-2.25.so]15,339,273,103 slow.c:main [/home/accts/aspnes/g/classes/223/notes/examples/profiling/slow]15,339,273,103 /home/accts/aspnes/g/classes/223/notes/examples/profiling/slow.c:main11,264,058,263 slow.c:copyEvenCharacters [/home/accts/aspnes/g/classes/223/notes/examples/profiling/slow]11,260,141,740 /usr/src/debug/glibc-2.25-123-gedcf13e25c/string/../sysdeps/x86_64/strlen.S:strlen [/usr/lib64/ld-2.25.so] 4,075,049,055 slow.c:replicate [/home/accts/aspnes/g/classes/223/notes/examples/profiling/slow] 4,074,048,083 /usr/src/debug/glibc-2.25-123-gedcf13e25c/string/../sysdeps/x86_64/multiarch/strcat-ssse3.S:__strcat_ssse3 [/usr/lib64/libc-2.25.so] 108,795 /usr/src/debug/glibc-2.25-123-gedcf13e25c/elf/rtld.c:_dl_start [/usr/lib64/ld-2.25.so]Since each function is charged for work done by its children, the top of the list includes various setup functions included automatically by the C compiler, followed bymain. Insidemain, we see that the majority of the work is done incopyEvenCharacters, with a substantial chunk inreplicate. The suspicious similarity in numbers suggests that most of these instructions incopyEvenCharacters are accounted for by calls tostrlen and inreplicate by calls to__strcat_sse3, which happens to be an assembly-language implementation ofstrcat (hence the.S in the source file name) that uses the specialSSE instructions in the x86 instruction set to speed up copying.
We can confirm this suspicion by looking at later parts of the file, which annotate the source code with instruction counts.
The annotated version ofslow.c includes this annotated version ofreplicate, showing roughly 4 billion instructions executed in__strcat_sse3:
. char * . replicate(char *dest, const char *src, int n) 12 { . /* truncate dest */ 4 dest[0] = '\0'; . . /* BAD: each call to strcat requires walking across dest */ 400,050 for(int i = 0; i < n; i++) { 600,064 strcat(dest, src); 836 => /usr/src/debug/glibc-2.25-123-gedcf13e25c/elf/../sysdeps/x86_64/dl-trampoline.h:_dl_runtime_resolve_xsave (1x)4,074,048,083 => /usr/src/debug/glibc-2.25-123-gedcf13e25c/string/../sysdeps/x86_64/multiarch/strcat-ssse3.S:__strcat_ssse3 (100009x) . } . 2 return dest; 4 }Similarly, the annotated version ofcopyEvenCharacters shows that 11 billion instructions were executed instrlen:
. char * . copyEvenCharacters(char *dest, const char *src) 12 { . int i; . int j; . . /* BAD: Calls strlen on every pass through the loop */2,000,226 for(i = 0, j = 0; i < strlen(src); i += 2, j++) {11,260,056,980 => /usr/src/debug/glibc-2.25-123-gedcf13e25c/string/../sysdeps/x86_64/strlen.S:strlen (200021x) 825 => /usr/src/debug/glibc-2.25-123-gedcf13e25c/elf/../sysdeps/x86_64/dl-trampoline.h:_dl_runtime_resolve_xsave (1x)2,000,200 dest[j] = src[i]; . } . 10 dest[j] = '\0'; . 2 return dest; 8 }This gives a very strong hint for fixing the program: cut down on the cost of callingstrlen andstrcat.
FixingcopyEvenCharacters is trivial. Because the length ofsrc doesn’t change, we can callstrlen once and save the value in a variable:
char *copyEvenCharacters(char *dest,constchar *src){int i;int j;size_t len;/* GOOD: Calls strlen only once */ len = strlen(src);for(i =0, j =0; i < len; i +=2, j++) { dest[j] = src[i]; } dest[j] = '\0';return dest;}Fixingreplicate is trickier. The trouble with usingstrcat is that every time we callstrcat(dest, src),strcat has to scan down the entiredest string to find the end, which (a) gets more expensive asdest gets longer, and (b) involves passing over the same non-null initial characters over and over again each time we want to add a few more characters. The effect of this is that we turn what should be anO(n)-time process of generating a string ofn characters into something that looks more likeO(n2). We can fix this by using pointer arithmetic to keep track of the end ofdest ourselves, which also allows us to replacestrcat withmemcpy, which is likely to be faster since it doesn’t have to check for nulls. Here’s the improved version:
char *replicate(char *dest,constchar *src,int n){size_t len = strlen(src);char *tail = dest;/* GOOD: each call to memcpy only copies n*strlen(src) bytes */for(int i =0; i < n; i++, tail += len) { memcpy(tail, src, len); }/* tack on final null */ *tail = '\0';return dest;}The result of applying both of these fixes can be found infast.c. This runs much faster thanslow:
abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdacacacacacacacacacacabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd[399960 more]acacacacacacacacacacacacacacacacacacacac[199960 more]real 0m0.003suser 0m0.001ssys 0m0.001sgprofIf you can’t usevalgrind for profiling, don’t like the output you get from it, or are annoyed by the huge slowdown when profiling your program, you may be able to get similar results from an older programgprof, which is closely tied to thegcc compiler. Unlikevalgrind, which simulates an x86 CPU one machine-code instruction at a time,gprof works by havinggcc add extra code to your program to track function calls and do sampling at runtime to see where your program is spending its time. The cost of this approach is that you get a bit less accuracy. I have also foundgprof to be tricky to get working right on some operating systems.
Here’s a short but slow program for calculating the number of primes less than some limit passed asargv[1]:
#include<stdio.h>#include<stdlib.h>/* return 1 if n is prime, 0 otherwise */intisPrime(int n){int factor;if(n <2)return0;/* else */for(factor =2; factor < n; factor++) {if(n % factor ==0)return0; }/* else */return1;}/* return number of primes < n */intcountPrimes(int n){int i;int count; count =0;for(i =0; i < n; i++) {if(isPrime(i)) count++; }return count;}intmain(int argc,char **argv){if(argc !=2) { fprintf(stderr,"Usage: %s n\n", argv[0]);return1; } printf("%d\n", countPrimes(atoi(argv[1])));return0;}And now we’ll timecountPrimes 100000:
$ gcc -g3 -o countPrimes countPrimes.c $ time ./countPrimes 1000009592real 0m4.711suser 0m4.608ssys 0m0.004sThis shows that the program took just under five seconds of real time, of which most was spent in user mode and a very small fraction was spent in kernel (sys) mode. The user-mode part corresponds to the code we wrote and any library routines we call that don’t require special privileges from the operation system. The kernel-mode part will mostly be I/O (not much in this case). Real time is generally less useful than CPU time, because it depends on how loaded the CPU is. Also, none of these times are especially precise, because the program only gets charged for time on a context switch (when it switches between user and kernel mode or some other program takes over the CPU for a bit) or when the kernel decides to see what it is up to (typically every 10 milliseconds).
The overall cost is not too bad, but the reason I picked 100000 and not some bigger number was that it didn’t finish fast enough for larger inputs. We’d like to see why it is taking so long, to have some idea what to try to speed up. So we’ll compile it with the-pg option togcc, which insertsprofiling code that counts how many times each function is called and how long (on average) each call takes.
Because the profile is not very smart about shared libraries, we also including the--static option to force the resulting program to be statically linked. This means that all the code that is used by the program is baked into the executable instead of being linked in at run-time. (Normally we don’t do this because it makes for big executables and big running programs, since statically-linked libraries can’t be shared between more than one running program.)
$ gcc -pg --static -g3 -o countPrimes countPrimes.c $ time ./countPrimes 1000009592real 0m4.723suser 0m4.668ssys 0m0.000sHooray! We’ve made the program slightly slower. But we also just produced a filegmon.out that we can read withgprof. Note that we have to pass the name of the program so thatgprof can figure out which executable generatedgmon.out.
$ gprof countPrimesFlat profile:Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 100.00 4.66 4.66 100000 0.00 0.00 isPrime 0.00 4.66 0.00 1 0.00 4.66 countPrimes 0.00 4.66 0.00 1 0.00 4.66 main[...much explanatory text deleted]It looks like we are spending all of our time inisPrime, at least if we read the columns on the left. The per-call columns are not too helpful because of granularity:isPrime is too fast for the profiler to wake up and detect how long it runs for. The total columns are less suspicious because they are obtained by sampling: from time to time, the profiler looks and sees what function it’s in, and charges each function a fraction of the total CPU time proportional to how often it gets sampled. So we probable aren’t really spending zero time incountPrimes andmain, but the amount of time we do spend is small enough not to be detected.
This is handy because it means we don’t need to bother trying to speed up the rest of the program. We have two things we can try:
isPrime less.isPrime faster.Let’s start by seeing if we can makeisPrime faster.
WhatisPrime is doing is testing if a numbern is prime by the most direct way possible: dividing by all numbers less thann until it finds a factor. That’s a lot of divisions: ifn is indeed prime, it’s linear inn. Since division is a relatively expensive operation, the first thing to try is to get rid of some.
Here’s a revised version ofisPrime:
/* return 1 if n is prime, 0 otherwise */intisPrime(int n){int factor;if(n <2) {return0; }if(n %2 ==0) {/* special case for the only even prime */return n ==2; }/* else */for(factor =3; factor < n; factor+=2) {if(n % factor ==0)return0; }/* else */return1;}examples/profiling/countPrimesSkipEvenFactors.c
The trick is to check first ifn is divisible by2, and only test odd potential factors thereafter. This requires some extra work to handle 2, but maybe the extra code complexity will be worth it.
Let’s see how the timing goes:
$ gcc -pg --static -g3 -o countPrimes ./countPrimesSkipEvenFactors.c $ time ./countPrimes 1000009592real 0m2.608suser 0m2.400ssys 0m0.004s$ gprof countPrimesFlat profile:Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 100.00 2.29 2.29 100000 0.00 0.00 isPrime 0.00 2.29 0.00 1 0.00 2.29 countPrimes 0.00 2.29 0.00 1 0.00 2.29 main[...]Twice as fast! And the answer is still the same, too—this is important.
Can we test even fewer factors? Supposen has a non-trivial factorx. Thenn equalsx*y for somey which is also nontrivial. One ofx ory will be no bigger than the square root ofn. So perhaps we can stop when we reach the square root ofn,
Let’s try it:
#include<math.h>/* return 1 if n is prime, 0 otherwise */intisPrime(int n){int factor;if(n <2) {return0; }if(n %2 ==0) {/* special case for the only even prime */return n ==2; }/* else */for(factor =3; factor < sqrt(n)+1; factor+=2) {if(n % factor ==0)return0; }/* else */return1;}}examples/profiling/countPrimesSqrt.c
I added+1 to the return value ofsqrt both to allow forfactor to be equal to the square root ofn, and because the output ofsqrt is not exact, and it would be embarrassing if I announced that 25 was prime because I stopped at 4.9999999997.
Using the math library not only requires including<math.h> but also requires compiling with the-lm flag after all.c or.o files, to link in the library routines:
$ gcc -pg --static -g3 -o countPrimes ./countPrimesSqrt.c -lm$ time ./countPrimes 100000078498real 0m1.008suser 0m0.976ssys 0m0.000s$ gprof countPrimesFlat profile:Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls ms/call ms/call name 50.00 0.02 0.02 100000 0.00 0.00 isPrime 50.00 0.04 0.02 __sqrt_finite 0.00 0.04 0.00 1 0.00 20.00 countPrimes 0.00 0.04 0.00 1 0.00 20.00 main[...]Whoosh!
Can we optimize further? Let’s see what happens on a bigger input:
$ time ./countPrimes 100000078498real 0m0.987suser 0m0.960ssys 0m0.000s$ gprof countPrimesFlat profile:Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls ms/call ms/call name 51.04 0.49 0.49 __sqrt_finite 44.79 0.92 0.43 1000000 0.00 0.00 isPrime 3.65 0.96 0.04 sqrt 0.52 0.96 0.01 1 5.00 435.00 main 0.00 0.96 0.00 1 0.00 430.00 countPrimes[...]This is still very good, although we’re spending a lot of time insqrt (more specifically, its internal helper routine__sqrt_finite). Can we do better?
Maybe moving thesqrt out of the loop inisPrime will make a difference:
/* return 1 if n is prime, 0 otherwise */intisPrime(int n){int factor;int sqrtValue;if(n <2) {return0; }if(n %2 ==0) {/* special case for the only even prime */return n ==2; }/* else */ sqrtValue = sqrt(n) +1;for(factor =3; factor < sqrtValue; factor+=2) {if(n % factor ==0)return0; }/* else */return1;}examples/profiling/countPrimesSqrtOutsideLoop.c
$ gcc -pg --static -g3 -o countPrimes ./countPrimesSqrtOutsideLoop.c -lm$ time ./countPrimes 100000078498real 0m0.413suser 0m0.392ssys 0m0.000s$ gprof countPrimesFlat profile:Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls ms/call ms/call name 97.44 0.38 0.38 1000000 0.00 0.00 isPrime 2.56 0.39 0.01 1 10.00 390.00 countPrimes 0.00 0.39 0.00 1 0.00 390.00 main[...]This worked! We are now spending so little time insqrt that the profiler doesn’t notice it.
What if we get rid of the call tosqrt and test iffactor * factor <= n instead? This way we could dump the math library:
/* return 1 if n is prime, 0 otherwise */intisPrime(int n){int factor;if(n <2) {return0; }if(n %2 ==0) {/* special case for the only even prime */return n ==2; }/* else */for(factor =3; factor*factor <= n; factor+=2) {if(n % factor ==0)return0; }/* else */return1;}examples/profiling/countPrimesSquaring.c
$ gcc -pg --static -g3 -o countPrimes ./countPrimesSquaring.c$ time ./countPrimes 100000078498real 0m0.450suser 0m0.428ssys 0m0.000sThis is slower, but not much slower. We might need to decide how much we care about avoiding floating-point computation in our program.
At this point we could decide thatcountPrimes is fast enough, or maybe we could look for further improvements, say, by testing out many small primes at the beginning instead of just2, callingisPrime only on odd values ofi, or reading a computational number theory textbook to find out how the cool kids do this.
Note that in some cases going all-out to improve performance may be more trouble than it’s worth. A reasonable strategy for code for your own use might be to start running one version and make improvements on a separate copy while it’s running. If the first version finishes before you are done writing new code, it’s probably fast enough.
We didn’t use any optimization flags for this example, because the optimizer can do a lot of rewriting that can make the output of the profiler confusing. For example, at high optimization levels, the compiler will often avoid function-call overhead by inserting the body of a helper function directly into its caller. But this can make a big difference in performance, so in real life you will want to compile with optimization turned on. Here’s how the performance ofcountPrimes 100000 is affected by optimization level:
| Version | No optimization | With -O1 | With -O2 | With -O3 |
|---|---|---|---|---|
countPrimes.c | 4.600 | 4.060 | 3.928 | 3.944 |
countPrimesSkipEvenFactors.c | 2.260 | 1.948 | 1.964 | 1.984 |
countPrimesSqrt.c | 0.036 | 0.028 | 0.028 | 0.028 |
countPrimesSqrtOutsideLoop.c | 0.012 | 0.012 | 0.008 | 0.008 |
countPrimesSquaring.c | 0.012 | 0.012 | 0.008 | 0.012 |
In each case, the reported time is the sum of user and system time in seconds.3
For the smarter routines, more optimization doesn’t necessarily help, although some of this may be experimental error since I was too lazy to get a lot of samples by running each program more than once, and the times for the faster programs are so small that granularity is going to be an issue.
Here’s the same table usingcountPrimes 10000000 on the three fastest programs:
| Version | No optimization | With -O1 | With -O2 | With -O3 |
|---|---|---|---|---|
countPrimesSqrt.c | 24.236 | 18.840 | 18.720 | 18.564 |
countPrimesSqrtOutsideLoop.c | 9.388 | 9.364 | 9.368 | 9.360 |
countPrimesSquaring.c | 9.748 | 9.248 | 9.236 | 9.160 |
Again there are the usual caveats that I am a lazy person and should probably be doing more to deal with sampling and granularity issues, but if you believe these numbers, we actually win by going tocountPrimesSquaring once the optimizer is turned on. I suspect that it is benefiting fromstrength reduction, which would generate the productfactor*factor inisPrime incrementally using addition rather than multiplying from scratch each time.
It’s also worth noting that the optimizer works better if we leave a lot of easy optimization lying around. ForcountPrimesSqrt.c, my guess is that most of the initial gains are from avoiding function call overhead onsqrt by compiling it in-line. But even the optimizer is not smart enough to recognize that we are computing the same value over and over again, so we still win by pullingsqrt out of the loop incountPrimesSqrtOutsideLoop.c.
If I wanted to see if my guesses about the optimizer were correct, I could usegcc -S and look at the assembler code. But see earlier comments about laziness.
When you are programming, you will make mistakes. If you program long enough, these will eventually include true acts of boneheadedness like accidentally deleting all of your source files. You are also likely to spend some of your time trying out things that don’t work, at the end of which you’d like to go back to the last version of your program that did work. All these problems can be solved by using aversion control system.
There are at least five respectable version control systems installed on the Zoo:rcs,cvs,svn,hg, andgit. If you are familiar with any of them, you should use that. If you have to pick one from scratch, I recommend usinggit. A brief summary ofgit is given below. For more details, see the tutorials available athttp://git-scm.com.
Typically you rungit inside a directory that holds some project you are working on (say,hw1). Before you can do anything withgit, you will need to create therepository, which is a hidden directory.git that records changes to your files:
$ mkdir git-demo$ cd git-demo$ git initInitialized empty Git repository in /home/classes/cs223/class/aspnes.james.ja54/git-demo/.git/Now let’s create a file and add it to the repository:
$ echo 'int main(int argc, char **argv) { return 0; }' > tiny.c$ git add tiny.cThegit status command will tell us that Git knows abouttiny.c, but hasn’t commited the changes to the repository yet:
$ git status# On branch master## Initial commit## Changes to be committed:# (use "git rm --cached <file>..." to unstage)## new file: tiny.c#Thegit commit command will commit the actual changes, along with a message saying what you did. For short messages, the easiest way to do this is to include the message on the command line:
$ git commit -a -m"add very short C program"[master (root-commit) 5393616] add very short C program Committer: James Aspnes <ja54@tick.zoo.cs.yale.edu>Your name and email address were configured automatically basedon your username and hostname. Please check that they are accurate.You can suppress this message by setting them explicitly: git config --global user.name "Your Name" git config --global user.email you@example.comIf the identity used for this commit is wrong, you can fix it with: git commit --amend --author='Your Name <you@example.com>' 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 tiny.cThe-a argument tells Git to include any changes I made to files it already knows about. The-m argument sets the commit message.
Because this is the first time I ever did a commit, and because I didn’t tell Git who I was before, it complains that its guess for my name and email address may be wrong. It also tells me what to do to get it to shut up about this next time:
$ git config --global user.name "James Aspnes"$ git config --global user.email "aspnes@cs.yale.edu"$ git commit --amend --author="James Aspnes <aspnes@cs.yale.edu>" -m"add a very short C program"[master a44e1e1] add a very short C program 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 tiny.cNote that I repeated the-m business togit commit --amend; if I hadn’t, it would have run the default editor (vim) to let me edit my commit message. If I don’t likevim, I can change the default usinggit config --global core.editor, e.g.:
$ git config --global core.editor "emacs -nw"I can see what commits I’ve done so far usinggit log:
$ git logcommit a44e1e195de4ce785cd95cae3b93c817d598a9eeAuthor: James Aspnes <aspnes@cs.yale.edu>Date: Thu Dec 29 20:21:21 2011 -0500 add a very short C programSuppose I edittiny.c using my favorite editor to turn it into the classic hello-world program:
I can see what files have changed usinggit status:
$ git status# On branch master# Changed but not updated:# (use "git add <file>..." to update what will be committed)# (use "git checkout -- <file>..." to discard changes in working directory)## modified: tiny.c#no changes added to commit (use "git add" and/or "git commit -a")Notice how Git reminds me to usegit commit -a to include these changes in my next commit. I can also dogit add tiny.c if I just want include the changes totiny.c (maybe I made changes to a different file that I want to commit separately), but usually that’s too much work.
If I want to know the details of the changes since my last commit, I can dogit diff:
$ git diffdiff --git a/tiny.c b/tiny.cindex 0314ff1..f8d9dcd 100644--- a/tiny.c+++ b/tiny.c@@ -1 +1,8 @@-int main(int argc, char **argv) { return 0; }+#include <stdio.h>++int +main(int argc, char **argv)+{+ puts("hello, world");+ return 0;+}Since I like these changes, I do a commit:
$ git commit -a -m"expand previous program to hello world"[master 13a73be] expand previous program to hello world 1 files changed, 8 insertions(+), 1 deletions(-)Now there are two commits in my log:
$ git log | tee /dev/nullcommit 13a73bedd3a48c173898d1afec05bd6fa0d7079aAuthor: James Aspnes <aspnes@cs.yale.edu>Date: Thu Dec 29 20:34:06 2011 -0500 expand previous program to hello worldcommit a44e1e195de4ce785cd95cae3b93c817d598a9eeAuthor: James Aspnes <aspnes@cs.yale.edu>Date: Thu Dec 29 20:21:21 2011 -0500 add a very short C programYou can rename a file withgit mv. This is just like regularmv, except that it tells Git what you are doing.
$ git mv tiny.c hello.c$ git status# On branch master# Changes to be committed:# (use "git reset HEAD <file>..." to unstage)## renamed: tiny.c -> hello.c#These changes don’t get written to the repository unless you do anothergit commit:
$ git commit -a -m"give better name to hello program"[master 6d2116c] give better name to hello program 1 files changed, 0 insertions(+), 0 deletions(-) rename tiny.c => hello.c (100%)To add a file, create it and then callgit add:
$ cp hello.c goodbye.c$ git status# On branch master# Untracked files:# (use "git add <file>..." to include in what will be committed)## goodbye.cnothing added to commit but untracked files present (use "git add" to track)$ git add goodbye.c$ git commit -a -m"we need a second program to say goodbye"[master 454b24c] we need a second program to say goodbye 1 files changed, 8 insertions(+), 0 deletions(-) create mode 100644 goodbye.cTo remove a file, usegit rm:
$ git rm goodbye.c $ git status# On branch master# Changed but not updated:# (use "git add/rm <file>..." to update what will be committed)# (use "git checkout -- <file>..." to discard changes in working directory)## deleted: goodbye.c#no changes added to commit (use "git add" and/or "git commit -a")$ git commit -a -m"no, goodbye.c was a bad idea"[master defa0e0] no, goodbye.c was a bad idea 1 files changed, 0 insertions(+), 8 deletions(-) delete mode 100644 goodbye.cIf you make a mistake, you can back out using the repository. Here I will delete myhello.c file and then get it back usinggit checkout -- hello.c:
$ rm hello.c$ ls$ git checkout -- hello.c$ lshello.cI can also get back old versions of files by putting the commit id before the--:
$ git checkout a44e1 -- tiny.c$ lshello.c tiny.cThe commit id can be any unique prefix of the ridiculously long hex name shown bygit log.
Having recoveredtiny.c, I will keep it around by adding it to a new commit:
$ git commit -a -m"keep tiny.c around"[master 23d6219] keep tiny.c around 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 tiny.cSuppose I commit a change that I didn’t want to make. For example, let’s suppose I decide to add some punctuation to the greeting inhello.c but botch my edit:
$ vim hello.c$ git commit -a -m"add exclamation point"[master f40d8d3] add exclamation point 1 files changed, 1 insertions(+), 1 deletions(-)Only now does it occur to me to test my program:
$ gcc -o hello hello.c$ ./hellohello, wolrd!Disaster!
I can usegit diff to see what went wrong. The command below compares the current working directory toHEAD^, the commit before the most recent commit:4
$ git diff HEAD^ | tee /dev/nulldiff --git a/hello.c b/hello.cindex f8d9dcd..dc227a8 100644--- a/hello.c+++ b/hello.c@@ -3,6 +3,6 @@ int main(int argc, char **argv) {- puts("hello, world");+ puts("hello, wolrd!"); return 0; }And I see my mistake leaping out at me on the new line I added (whichgit diff puts a+ in front of). But now what do I do? I already commited the change, which means that I can’t get it out of the history.5
Instead, I usegit revert onHEAD, the most recent commit:
$ git revert HEAD[master fca3166] Revert "add exclamation point" 1 files changed, 1 insertions(+), 1 deletions(-)(Not shown here is where it popped up avim session to let me edit the commit message; I just hit:x<ENTER> to get out of it without changing the default.)
Now everything is back to the way it was before the bad commit:
$ ./hellohello, worldRunninggit log will now show me the entire history of my project, newest commits first:
fca3166a697c6d72fb9e8aec913bb8e36fb5fe4e Revert "add exclamation point"f40d8d386890103abacd0bf4142ecad62eed5aeb add exclamation point23d6219c9380ba03d9be0672f0a7b25d18417731 keep tiny.c arounddefa0e0430293ca910f077d5dd19fccc47ab0521 no, goodbye.c was a bad idea454b24c307121b5a597375a99a37a825b0dc7e81 we need a second program to say goodbye6d2116c4c72a6ff92b8b276eb88ddb556d1b8fdd give better name to hello program13a73bedd3a48c173898d1afec05bd6fa0d7079a expand previous program to hello worlda44e1e195de4ce785cd95cae3b93c817d598a9ee add a very short C programIf I want to look at an old version (say, after I createdgoodbye.c), I can go back to it usinggit checkout:
$ git checkout 454b2Note: checking out '454b2'.You are in 'detached HEAD' state. You can look around, make experimentalchanges and commit them, and you can discard any commits you make in thisstate without impacting any branches by performing another checkout.If you want to create a new branch to retain commits you create, you maydo so (now or later) by using -b with the checkout command again. Example: git checkout -b new_branch_nameHEAD is now at 454b24c... we need a second program to say goodbye$ lsgoodbye.c hello hello.cNow I have bothgoodbye.c andhello.c, as well as my compiled programhello, since I didn’t tell Git about it. Note that I also got lots of horrendous warnings about the fact that I am living in the past and shouldn’t expect to make any permanent changes here.
To go back to the last commit, usegit checkout master:
$ git checkout masterPrevious HEAD position was 454b24c... we need a second program to say goodbyeSwitched to branch 'master'$ lshello hello.c tiny.cAll Git commands take a--help argument that brings up their manual page. There is also extensive documentation athttp://git-scm.com.
The submit command is is found in/c/cs223/bin on the Zoo. Here is the documentation (adapted from comments in the script):
submit assignment-number file(s)unsubmit assignment-number file(s)check assignment-numbermakeit assignment-number [file]protect assignment-number file(s)unprotect assignment-number file(s)retrieve assignment-number file(s)testit assignment-number testdiffit assignment-number file(s)revert assignment-number -r[version number] file(s)results assignment-numberThe submit program can be invoked in eleven different ways: /c/cs223/bin/submit 1 Makefile tokenize.c unique.c time.logsubmits the named source files as your solution to Homework #1; /c/cs223/bin/check 2lists the files that you have submitted for Homework #2; /c/cs223/bin/unsubmit 3 error.submit bogus.solutiondeletes the named files that you had submitted previously for Homework #3(i.e., withdraws them from submission, which is useful if you accidentallysubmit the wrong file); /c/cs223/bin/makeit 4 tokenize uniqueruns "make" on the files that you submitted previously for Homework #4; /c/cs223/bin/protect 5 tokenize.c time.logprotects the named files that you submitted previously for Homework #5 (sothey cannot be deleted accidentally); /c/cs223/bin/unprotect 6 unique.c time.logunprotects the named files that you submitted previously for Homework #6(so they can be deleted); /c/cs223/bin/retrieve 7 Csquash.cretrieves copies of the named files that you submitted previously for Homework #7; /c/cs223/bin/testit 8 BigTestruns the test script /c/cs223/Hwk8/test.BigTest on the submission directory; /c/cs223/bin/diffit 9 erroneous.ccompares the submitted version of a file with the version in the local directory; /c/cs223/bin/revert 10 -r1.1 overWritten.creverts a submitted file to a previous version 1.1; and /c/cs223/bin/results 11shows the grading results for Homework #11, once it has been graded.Theretrieve command supports a-d argument that can be used to retrieve the version of a file as of a particular time, as in
/c/cs223/bin/retrieve 7 -d"2020/03/01 17:57" Csquash.cThis will also display the version used by the internal RCS version control script for use withrevert. Note thatretrieve makes a copy of the file in the current working directory, whilerevert updates the copy in the submission directory (including restoring its modification time). This can be used to avoid late penalties if you forget to useprotect and accidentally overwrite a file in your submission directory after the assignment deadline.
Thesubmit program will only work if there is a directory with your name and login on it under/c/cs223/class.
The C programming language was developed at Bell Laboratories in the early 1970s as the system programming language for Unix, based on the earlier and even more primitive languages BCPL and B. When originally developed, it was targeted at machines that were extremely limited by modern standards: the first Unix implementation (and the B compiler that supported it) ran on a DEC PDP-7 with only 8192 18-bit words of memory (Dennis M. Ritchie, The development of the C language, in Thomas J. Bergin, Jr., and Richard G. Gibson, Jr., History of Programming Languages-II ed., ACM Press, 1996). So using as few resources as possible, both in the compiler and in the resulting code, was a priority.
This priority is reflected in the features (and lack of features) of C, and is partly responsible for its success. Programs written in C place almost no demands on the system they run on and give the programmer nearly complete control over their execution: this allows programs that were previously written in assembly language, like operating system kernels and device drivers, to be implemented in C. So C is often the first language ported to any new architecture, and many higher-level languages are either executed using interpreters written in C or use C as in intermediate language in the compilation process.
Since its initial development, C has gone through four major versions:
Unfortunately, C99 and C11 both exemplify the uselessness of standards committees in general and theISO in particular. Because the ISO has no power to enforce standards on compiler writers, and because they will charge you CHF 198 just to look at the C11 standard, many compiler writers have ignored much of C99 and C11. In particular, Microsoft pretty much gave up on adding any features after ANSI C, and support for C99 and C11 is spotty ingcc andclang, the two dominant open source C compilers. So if you want to write portable C code, it is safest to limit yourself to features in ANSI C.
For this class, we will permit you to use any feature of C thatgcc supports, which includes all features of ANSI C and most features of later standards.
A C program consists of one or more files (which act a little bit like modules in more structured programming languages, each of which typically containsdefinitions offunctions, each of which consists ofstatements, which are eithercompound statements likeif,while, etc. orexpressions that typically perform some sort of arithmetic or call other functions. Files may also includedeclarations of global variables (not recommended), and functions will often contain declarations of local variables that can only be used inside that function.
Here is a typical small C program that sums a range of integers. Since this is our first real program, it’s a little heavy on the comments (shown between/* and*/).
#include<stdio.h>/* This is needed to get the declarations of fprintf and printf */#include<stdlib.h>/* This is needed to get the declaration of atoi *//* Return the sum of all integers i * such that start <= i and i < end. */intsumRange(int start,int end){int i;/* loop variable */int sum;/* sum of all values so far *//* a mathematician would use a formula for this, * but we are computer programmers! */ sum =0;/* The three parts of the header for this loop mean: * 1. Set i to start initially. * 2. Keep doing the loop as long as i is less than end. * 3. After each iteration, add 1 to i. */for(i = start; i < end; i++) { sum += i;/* This adds i to sum */ }/* This exits the function immediately, * sending the value of sum back to the caller. */return sum;}intmain(int argc,char **argv){int start;/* initial value in range */int end;/* one past the last value in the range *//* This tests for the wrong number of arguments. * The != operator returns true (1) if its arguments are not equal, * and false (0) otherwise. * Note that the program name itself counts as an argument * (which is why we want the argument count to be 3) * and appears in position 0 in the argument vector * (which means we can get it using argv[0]). */if(argc !=3) { fprintf(stderr,"Usage: %s\n start end", argv[0]);return1; }/* Convert start and end positions from strings to ints */ start = atoi(argv[1]); end = atoi(argv[2]);/* Call sumRange and print the result */ printf("sumRange(%d, %d) = %d\n", start, end, sumRange(start, end));return0;}This is what the program does if we compile and run it:
$ gcc -g -Wall -o sumRange sumRange.c$ ./sumRange 1 100sumRange(1, 100) = 4950ThesumRange.c program contains two functions,sumRange andmain. ThesumRange function does the actual work, whilemain is the main routine of the program that gets called with the command-line arguments when the program is run. Every C program must have a routine namedmain with these particular arguments.
In addition,main may call three library functions,fprintf (which in this case is used to generate error messages),printf (which generates ordinary output), andatoi (which is used to translate the command-line arguments into numerical values). These functions must all be declared before they can be used. In the case ofsumRange, putting the definition ofsumRange before the definition ofmain is enough. For the library routines, theinclude filesstdio.h andstdlib.h contain declarations of the functions that contain enough information about there return types and arguments that the compiler knows how to generate machine code to call them. These files are included insumRange.c by theC preprocessor, which pastes in the contents of any file specified by the#include command, strips out any comments (delimited by/* and*/, or by// and the end of the line if you are using C99), and does some other tricks that allow you to muck with the source code before the actual compiler sees it (seeMacros). You can see what the output of the preprocessor looks like by calling the C compiler with the-E option, as ingcc -E sumRange.c.
Thebody of each function consists of somevariable declarations followed by a sequence ofstatements that tell the computer what to do. Unlike some languages, every variable used in a C program must be declared. A declaration specifies thetype of a variable, which tells the compiler how much space to allocate for it and how to interpret some operations on its value. Statements may becompound statements like theif statement inmain that executes its body only if the program is called with the wrong number of command-line arguments or thefor loop insumRange that executes its body as long as the test in its header remains true; or they may besimple statements that consist of a singleexpression followed by a semicolon.
Anexpression is usually either a bare function call whose value is discarded (for example, the calls tofprintf andprintf inmain), or an arithmetic expression (which may include function calls, like the calls toatoi or inmain) whose value is assigned to some variable using theassignment operator= or sometimes variants like+= (which is shorthand for adding a value to an existing variable:x += y is equivalent tox = x+y).
When you compile a C program, after running the preprocessor, the compiler generatesassembly language code that is a human-readable description of the ultimate machine code for your target CPU. Assembly language strips out all the human-friendly features of your program and reduces it to simple instructions usually involving moving things from one place to the other or performing a single arithmetic operation. For example, the C line
gets translated into x86 assembly as
These three operations copy the value ofy into the CPU register%edi, add 1 to the%edi register, and then copy the value back intox. This corresponds directly to what you would have to do to evaluatex = y + 1 if you could only do one very basic operation at a time and couldn’t do arithmetic operations on memory locations: fetchy, add 1, storex. Note that the CPU doesn’t know about the namesy andx; instead, it computes their addresses by adding -24 and -28 respectively to the base pointer register%rbp. This is why it can be hard to debug compiled code unless you tell the compiler to keep around extra information.
For an arbitrary C program, if you are usinggcc, you can see what your code looks like in assembly language using the-S option. For example,gcc -S sumRange.c will create a filesumRange.s that looks like this:
.file"sumRange.c".text.globlsumRange.typesumRange,@functionsumRange:.LFB0:.cfi_startprocpushl%ebp.cfi_def_cfa_offset8.cfi_offset5,-8movl%esp, %ebp.cfi_def_cfa_register5subl$16, %espmovl$0,-4(%ebp)movl8(%ebp), %eaxmovl%eax,-8(%ebp)jmp.L2.L3:movl-8(%ebp), %eaxaddl%eax,-4(%ebp)addl$1,-8(%ebp).L2:movl-8(%ebp), %eaxcmpl12(%ebp), %eaxjl.L3movl-4(%ebp), %eaxleave.cfi_restore5.cfi_def_cfa4,4ret.cfi_endproc.LFE0:.sizesumRange, .-sumRange.section.rodata.LC0:.string"Usage: %s\n start end".LC1:.string"sumRange(%d, %d) = %d\n".text.globlmain.typemain,@functionmain:.LFB1:.cfi_startprocpushl%ebp.cfi_def_cfa_offset8.cfi_offset5,-8movl%esp, %ebp.cfi_def_cfa_register5andl$-16, %espsubl$32, %espcmpl$3,8(%ebp)je.L6movl12(%ebp), %eaxmovl(%eax), %edxmovlstderr, %eaxmovl%edx,8(%esp)movl$.LC0,4(%esp)movl%eax, (%esp)callfprintfmovl$1, %eaxjmp.L7.L6:movl12(%ebp), %eaxaddl$4, %eaxmovl(%eax), %eaxmovl%eax, (%esp)callatoimovl%eax,24(%esp)movl12(%ebp), %eaxaddl$8, %eaxmovl(%eax), %eaxmovl%eax, (%esp)callatoimovl%eax,28(%esp)movl28(%esp), %eaxmovl%eax,4(%esp)movl24(%esp), %eaxmovl%eax, (%esp)callsumRangemovl%eax,12(%esp)movl28(%esp), %eaxmovl%eax,8(%esp)movl24(%esp), %eaxmovl%eax,4(%esp)movl$.LC1, (%esp)callprintfmovl$0, %eax.L7:leave.cfi_restore5.cfi_def_cfa4,4ret.cfi_endproc.LFE1:.sizemain, .-main.ident"GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2".section.note.GNU-stack,"",@progbitsYou usually don’t need to look at assembly language, but it can sometimes be enlightening to see what the compiler is doing with your code. One thing that I find interesting about this particular code (which is for the x86 architecture) is that most of the instructions aremovl, the x86 instruction for copying a 32-bit quantity from one location to another. So most of what this program is doing is copying data into the places expected by the library functions it is calling. Also noteworthy is that the beautiful compound statements likeif andfor that so eloquently express the intent of the programmer get turned into a pile of jump (jmp) and conditional jump (jl,je) instructions, the machine code versions of the often dangerous and confusinggoto statement. This is because CPUs are dumb: they don’t know how to carry out anif branch or a loop, and all they can do instead is be told to replace the value of their program counter register with some new value instead of just incrementing it as they usually do.
Assembly language is not the last stage in this process. Theassembler (as) is a program that translates the assembly language insumRange.s into machine code (which will be stored insumRange.o if we aren’t compiling a single program all at once). Machine code is not human-readable, and is close to the raw stream of bytes that gets stored in the computer’s memory to represent a running program. The missing parts are that the addresses of each function and global variables are generally left unspecified, so that they can be moved around to make room for other functions and variables coming from other files and from system libraries. The job of stitching all of these pieces together, putting everything in the right place, filling in any placeholder addresses, and generating theexecutable filesumRange that we can actually run is given to thelinkerld.
The whole process looks like this:
sumRange.c (source code) | v[preprocessor (cpp)] | vpreprocessed version of sumRange.c | v[compiler (gcc)] | vsumRange.s (assembly code) | v[assembler (as)] | vsumRange.o (machine code) | v[linker (ld)] <- system library (glibc.a) | vsumRange (executable)The good news is, you don’t actually have to run all of these steps yourself; instead,gcc will take care of everything for you, particularly for simple programs likesumRange.c that fit in a single file.
All data stored inside a computer is ultimately represented as a sequence ofbits, 0 or 1 values, typically organized intowords consisting of several 8-bitbytes.6
A typical desktop computer might have enough RAM to store232 bytes (4 gigabytes); the Zoo machines store235 bytes (32 gigabytes). However, theaddress space of a process might be much larger: on a 64-bit machine, the address space is264 bytes. There’s no way to store264 different addresses in235 bytes of RAM; instead, amemory mapper, typically built in to the CPU, translates the large addresses of the parts of the address space that are actually used into smaller addresses corresponding to actual RAM locations. In some cases, regions of memory that have not been used in a while will beswapped out to disk, leaving more RAM free for other parts of the process (or other processes). This technique is known asvirtual memory and is usually invisible to the programmer. The use of virtual memory can increase the available space beyond the size of the RAM a little bit, but if you try to run a process that is actively using significantly more space that can be stored in RAM, it will slow down dramatically, because disk drives are roughly ten million times slower than memory.
The most basic kind of data represents integer values from some bounded range. C supports severalinteger data types, varying in their size (and thus range), and whether or not they are considered to be signed. These are described in more detailbelow.
For numerical computation, integer data types can be inconvenient. So C also supportsfloating-point types that consist of a fixed-sizemantissa, which is essentially an integer, together with anexponent that is used to multiply the mantissa by2x for somex. These allow very small or very large values to be represented with small relative error, but do not allow exact computation because of the limited precision of the mantissa. Floating-point types are also describedbelow.
All other data is represented by converting it to either integer or floating-point numbers. For example, text characters in C are represented as small integer values, so that the character constant'z' representation a lower-case “z” is exactly the same as the integer constant122 (which is theASCII code for “z”). A string like"hi there" is represented by a sequence of 8-bit ASCII characters, with a special 0 character to mark the end of the string. Strings that go beyond the English characters available in the ASCII encoding are typically represented usingUnicode and encoded as sequences of bytes using a particular representation called UTF-8. The color of a pixel in an image might be represented as three 8-bit integers representing the intensity of red, green, and blue in the color, while an image itself might be a long sequence of such 3-byte RGB values. At the bottom, every operation applied to these more complex data types translates into a whole lot of copies and arithmetic operations on individual bytes and words.
From the CPU’s point of view, even much of this manipulation consists of operating on integers that happen to represent addresses instead of data. So when a C program writes a zero to the 19th entry in a sequence of 4-byte integers, somewhere in the implementation of this operation the CPU will be adding4 ⋅ 19 to a base address for the sequence to computer where to write this value. Unlike many higher-level languages, C allows the program direct access to address computations viapointer types, which are tricky enough to gettheir own chapter. Indeed, most of the structured types that C provides for representing more complicated data can best be understood as a thin layer of abstraction on top of pointers. We will see examples of these in later chapters as well.
For now, we concentrate on integer and floating-point types, and on the operations that can be applied to them.
Most variables in C programs tend to hold integer values, and indeed most variables in C programs tend to be the default-width integer typeint. Declaring a variable to have a particular integer type controls how much space is used to store the variable (any values too big to fit will be truncated) and specifies that the arithmetic on the variable is done using integer operations.
The standard C integer types are:
| Name | Typical size | Signed by default? |
|---|---|---|
char | 8 bits | unspecified |
short | 16 bits | signed |
int | 32 bits | signed |
long | 32 bits | signed |
long long | 64 bits | signed |
The typical size is for architectures like the Intel x86, which is the architecture used in most desktop and server machines. Some 64-bit machines might have 64-bitints andlongs, and some microcontrollers have 16-bitints. Particularly bizarre architectures might have even wilder sizes, but you are not likely to see this unless you program vintage 1970s supercomputers. The general convention is thatint is the most convenient size for whatever computer you are using and should be used by default.
Many compilers also support along long type that is usually twice the length of a long (making it 64 bits on x86 machines). This type was not officially added to the C standard prior to C99, so it may or may not be available if you insist on following the ANSI specification strictly.
If you need to know the exact size of each type, you can use thesizeof operator, which returns the number ofchars in a type. For example, on a typical machine,sizeof(int) will evaluate to4, andsizeof(long long) will evaluate to8. You can multiply by the constantCHAR_BIT, usually defined in/usr/include/limits.h, to translate these number to bits. However, if you are looking for a type that holds a particular number of bits, you are better off using aC99 fixed-width type likeint32_t.
Each of these types comes in signed and unsigned variants.
This controls the interpretation of some operations (mostly comparisons and shifts) and determines the range of the type: for example, anunsigned char holds values in the range 0 through 255 while asigned char holds values in the range -128 through 127, and in general an unsignedn-bit type runs from 0 through2n − 1 while the signed version runs from − 2n − 1 through2n − 1 − 1. The representation of signed integers usestwo’s-complement notation, which means that a positive valuex is represented as the unsigned valuex while a negative value − x is represented as the unsigned value2n − x. For example, if we had a peculiar implementation of C that used 3-bitints, the binary values and their interpretation asint orunsigned int would look like this:
| bits | asunsigned int | asint |
|---|---|---|
| 000 | 0 | 0 |
| 001 | 1 | 1 |
| 010 | 2 | 2 |
| 011 | 3 | 3 |
| 100 | 4 | -4 |
| 101 | 5 | -3 |
| 110 | 6 | -2 |
| 111 | 7 | -1 |
The reason we get one extra negative value for a signed integer type is this allows us to interpret the first bit as the sign, which makes life a little easier for whoever is implementing our CPU. Two useful features of this representation are:
Note that in order to make this work, we can’t detect overflow: when the CPU adds two 3-bit integers, it doesn’t know if we are adding7 + 6 = 111 + 110 = 1101 = 13 or( − 1) + ( − 2) = 111 + 110 = 101 = ( − 3). In both cases the result is truncated to101, which gives the incorrect answer5 when we are adding unsigned values.
This can often lead to surprising uncaught errors in C programs, although using more than 3 bits will make overflow less likely. It is usually a good idea to pick a size for a variable that is substantially larger than the largest value you expect the variable to hold (although most people just default toint), unless you are very short on space or time (larger values take longer to read and write to memory, and may make some arithmetic operations take longer).
Taking into account signed and unsigned versions, the full collection of integer types looks like this:
char | signed char | unsigned char |
short | unsigned short | |
int | unsigned int | |
long | unsigned long | |
long long | unsigned long long |
So these are all examples of declarations of integer variables:
int i;char c;signedchar temperature;/* degrees Celsius, only valid for Earth's surface */long netWorthInPennies;longlong billGatesNetWorthInPennies;unsignedshort shaveAndAHaircutTwoBytes;Forchars, whether the character is signed( − 128…127) or unsigned(0…255) is at the whim of the compiler. If it matters, declare your variables assigned char orunsigned char. For storing actual 8-bit characters that you aren’t doing arithmetic on, it shouldn’t matter.
There is a slight gotcha with character processing with the input functionsgetchar andgetc. These return the special valueEOF (defined instdio.h to be − 1) to indicate end of file. But255, which represents'ÿ' in the ISO Latin-1 alphabet and in Unicode, and which may also appear quite often in binary files, will map to − 1 if you put it in achar. So you should store the output of these functions in anint if you need to test for end of file. After you have done this test, it’s safe to store a non-end-of-file character in achar.
So far we have been assuming that overflow implicitly applies a (mod2b) operation, whereb is the number of bits in our integer data type. This works on many machines, but as of the C11 standard, this is defined behavior only forunsigned integer types. Forsigned integer types, the effect of overflow isundefined. This means that the result of adding two very large signedints could be arbitrary, and not only might depend on what CPU, compiler, and compiler options you are using, but might even vary from one execution of your program to another. In many cases this is not an issue, but undefined behavior is often exploited by compilers to speed up compiled code by omitting otherwise necessary instructions to force a particular outcome. This is especially true if you turn on the optimizer using the-O flag.
This means that you should not depend on reasonable behavior for overflow of signed types. Usually this is not a problem, because signed computations often represent real-world values where overflow will produce bad results anyway. For unsigned computations, the implicit modulo operation applied to overflow can be useful for some applications.
C99 provides astdint.h header file that defines integer types with known size independent of the machine architecture. So in C99, you can useint8_t instead ofsigned char to guarantee a signed type that holds exactly 8 bits, oruint64_t instead ofunsigned long long to get a 64-bit unsigned integer type. The full set of types typically defined areint8_t,int16_t,int32_t, andint64_t for signed integers anduint8_t,uint16_t,uint32_t, anduint64_t for unsigned integers. There are also types for integers that contain the fewest number of bits greater than some minimum (e.g.,int_least16_t is a signed type with at least 16 bits, chosen to minimize space) or that are the fastest type with at least the given number of bits (e.g.,int_fast16_t is a signed type with at least 16 bits, chosen to minimize time). Thestdint.h file also defines constants giving the minimum and maximum values of these and standard integer types; for example,INT_MIN andINT_MAX give the smallest and largest values that can be stored in anint.
All of these types are defined as aliases for standard integer types usingtypedef; the main advantage of usingstdint.h over defining them yourself is that if somebody ports your code to a new architecture,stdint.h should take care of choosing the right types automatically. The main disadvantage is that, like many C99 features,stdint.h is not universally available on all C compilers. Also, because these fixed-width types are a late addition to the language, the built-in routines for printing and parsing integers, as well as the mechanisms for specifying the size of an integer constant, are not adapted to deal with them.
If you do need to print or parse types defined instdint.h, the largerinttypes.h header defines macros that give the corresponding format strings forprintf andscanf. Theinttypes.h file includesstdint.h, so you do not need to include both. Below is an example of a program that uses the various features provided byinttypes.h andstdint.h.
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<inttypes.h>/* example of using fixed-width types *//* largest value we can apply 3x+1 to without overflow */#define MAX_VALUE ((UINT64_MAX - 1) / 3)intmain(int argc,char **argv){uint64_t big;if(argc !=2) { fprintf(stderr,"Usage: %s number\n", argv[0]);return1; }/* parse argv[1] as a uint64_t *//* SCNu64 expands to the format string for scanning uint64_t (without the %) *//* We then rely on C concatenating adjacent string constants. */ sscanf(argv[1],"%" SCNu64, &big);/* do some arithmetic on big */while(big !=1) {/* PRIu64 expands to the format string for printing uint64_t */ printf("%" PRIu64"\n", big);if(big %2 ==0) { big /=2; }elseif(big <= MAX_VALUE) { big =3*big +1; }else {/* overflow! */ puts("overflow");return1; } } puts("Reached 1");return0;}size_t andptrdiff_tThe type aliasessize_t andptrdiff_t are provided instddef.h to represent the return types of thesizeof operator andpointer subtraction. On a 32-bit architecture,size_t will be equivalent to the unsigned 32-bit integer typeuint32_t (or justunsigned int) andptrdiff_t will be equivalent to the signed 32-bit integer typeint32_t (int). On a 64-bit architecture,size_t will be equivalent touint64_t andptrdiff_t will be equivalent toint64_t.
The place where you will most often seesize_t is as an argument tomalloc, where it gives the number of bytes to allocate.
Becausestdlib.h includesstddef.h, it is often not necessary to includestddef.h explicitly.
Constant integer values in C can be written in any of four different ways:
0,1,-127,9919291,97.0, e.g. 01 for 1,010 for 8,0777 for 511,0141 for 97. Octal is not used much any more, but it is still conventional for representing Unix file permissions.0x. The lettersa throughf are used for the digits 10 through 15. For example,0x61 is another way to write 97.'a' is 97.7 Unlike languages with separate character types, C characters are identical to integers; you can (but shouldn’t) calculate972 by writing'a'*'a'. You can also store a character in a location with any integer type.Except for character constants, you can insist that an integer constant is unsigned or long by putting au orl after it. So1ul is anunsigned long version of 1. By default integer constants are (signed)ints. Forlong long constants, usell, e.g., theunsigned long long constant0xdeadbeef01234567ull. It is also permitted to write thel asL, which can be less confusing if thel looks too much like a1.
Some examples:
'a' | int |
97 | int |
97u | unsigned int |
0xbea00d1ful | unsigned long, written in hexadecimal |
0777s | short, written in octal |
A curious omission is that there is no way to write a binary integer directly in C. So if you want to write the bit pattern 00101101, you will need to encode it in hexadecimal as0x2d (or octal as055). Another potential trap is that leading zeros matter:012 is an octal value representing the number most people call 10.
Having a lot of numeric constants in your program—particularly if the same constant shows up in more than one place—is usually a sign of bad programming. There are a few constants, like 0 and 1, that make sense on their own, but many constant values are either mostly arbitrary, or might change if the needs of the program change. It’s helpful to assign these constants names that explain their meaning, instead of requiring the user to guess why there is a37 here or an0x1badd00d there. This is particularly important if the constants might change in later versions of the program, since even though you could change every37 in your program into a38, this might catch other37 values that have different intended meanings.
For example, suppose that you have a function (call itgetchar) that needs to signal that sometimes it didn’t work. The usual way is to return a value that the function won’t normally return. Now, you could just tell the user what value that is:
/* get a character (as an `int` ASCII code) from `stdin` *//* return -1 on end of file */int getchar(void);and now the user can write
But then somebody reading the code has to remember that-1 means “end of file” and not “signed version of0xff” or “computer room on fire, evacuate immediately.” It’s much better to define a constantEOF that happens to equal-1, because among other things if you change the special return value fromgetchar later then this code will still work (assuming you fixed the definition ofEOF):
So how do you declare a constant in C? The traditional approach is to use the C preprocessor, the same tool that gets run before the compiler to expand out#include directives. To defineEOF, the file/usr/include/stdio.h includes the text
What this means is that whenever the charactersEOF appear in a C program as a separate word (e.g. in1+EOF*3 but not inappurtenancesTherEOF), then the preprocessor will replace them with the characters(-1). The parentheses around the-1 are customary to ensure that the-1 gets treated as a separate constant and not as part of some larger expression. So from the compiler’s perspective,EOF really is-1, but from the programmer’s perspective, it’s end-of-file. This is a special case of the C preprocessor’smacro mechanism.
In general, any time you have a non-trivial constant in a program, it should be#defined. Examples are things like array dimensions, special tags or return values from functions, maximum or minimum values for some quantity, or standard mathematical constants (for example,/usr/include/math.h definesM_PI as the mathematical constant pi to umpteen digits). This allows you to write
char buffer[MAX_FILENAME_LENGTH+1]; area = M_PI*r*r;if(status == COMPUTER_ROOM_ON_FIRE) { evacuate(); }instead of
char buffer[513]; area =3.141592319*r*r;/* not the correct value of pi */if(status ==136) { evacuate(); }which is just an invitation to errors (including the one in the area computation).
Liketypedefs,#defines that are intended to be globally visible are best done in header files; in large programs you will want to#include them in many source files. The usual convention is to write#defined names in all-caps to remind the user that they are macros and not real variables.
The usual+ (addition),- (negation or subtraction), and* (multiplication) operators work on integers pretty much the way you’d expect. The only caveat is that if the result lies outside of the range of whatever variable you are storing it in, it will be truncated instead of causing an error:
unsignedchar c; c = -1;/* sets c = 255 */ c =255 +255;/* sets c = 254 */ c =256 *1772717;/* sets c = 0 */This can be a source of subtle bugs if you aren’t careful. The usual giveaway is that values you thought should be large positive integers come back as random-looking negative integers.
Division (/) of two integers also truncates:2/3 is 0,5/3 is 1, etc. For positive integers it will always round down.
Prior to C99, if either the numerator or denominator was negative, the behavior was unpredictable and depended on what your processor chose to do. In practice this meant you should never use/ if one or both arguments might be negative. The C99 standard specified that integer division always removes the fractional part, effectively rounding toward 0; so(-3)/2 is-1,3/-2 is-1, and(-3)/-2 is1.
There is also a remainder operator% with e.g.2%3 = 2,5%3 = 2,27 % 2 = 1, etc. The sign of the modulus is ignored, so2%-3 is also2. The sign of the dividend carries over to the remainder:(-3)%2 and(-3)%(-2) are both-1. The reason for this rule is that it guarantees thaty == x*(y/x) + y%x is always true.
In addition to the arithmetic operators, integer types supportbitwise logical operators that apply some Boolean operation to all the bits of their arguments in parallel. What this means is that the i-th bit of the output is equal to some operation applied to the i-th bit(s) of the input(s). The bitwise logical operators are~ (bitwise negation: used with one argument as in~0 for the all-1’s binary value),& (bitwise AND), ‘|’ (bitwise OR), and ‘^’ (bitwise XOR, i.e. sum mod 2). These are mostly used for manipulating individual bits or small groups of bits inside larger words, as in the expressionx & 0x0f, which strips off the bottom four bits stored inx.
Examples:
x | y | expression | value |
|---|---|---|---|
| 0011 | 0101 | x&y | 0001 |
| 0011 | 0101 | x|y | 0111 |
| 0011 | 0101 | x^y | 0110 |
| 0011 | 0101 | ~x | 1100 |
The shift operators<< and>> shift the bit sequence left or right:x << y produces the valuex ⋅ 2y (ignoring overflow); this is equivalent to shifting every bit inxy positions to the left and filling iny zeros for the missing positions. In the other direction,x >> y produces the value⌊x ⋅ 2−y⌋ by shiftingxy positions to the right. The behavior of the right shift operator depends on whetherx is unsigned or signed; for unsigned values, it shifts in zeros from the left end always; for signed values, most implementations shift in additional copies of the leftmost bit (the sign bit). This makesx >> y have the same sign asx ifx is signed. However, this behavior isnot guaranteed by the C standard, it’s generally best not to apply right shifts to negative values.
Ify is negative, the behavior of the shift operators is undefined.
Examples (unsigned char x):
x | y | x << y | x >> y |
|---|---|---|---|
| 00000001 | 1 | 00000010 | 00000000 |
| 11111111 | 3 | 11111000 | 00011111 |
Examples (signed char x):
x | y | x << y | x >> y |
|---|---|---|---|
| 00000001 | 1 | 00000010 | 00000000 |
| 11111111 | 3 | 11111000 | 11111111 |
Shift operators are often used with bitwise logical operators to set or extract individual bits in an integer value. The trick is that(1 << i) contains a 1 in thei-th least significant bit and zeros everywhere else. Sox & (1<<i) is nonzero if and only ifx has a 1 in thei-th place. This can be used to print out an integer in binary format (which standardprintf won’t do).
The following program gives an example of this technique. For example, when called as./testPrintBinary 123, it will print00000000000000000000000001111011 followed by a newline.
#include<stdio.h>#include<stdlib.h>/* print out all bits of n */voidprint_binary(unsignedint n){unsignedint mask =0;/* this grotesque hack creates a bit pattern 1000... *//* regardless of the size of an unsigned int */ mask = ~mask ^ (~mask >>1);for(; mask !=0; mask >>=1) { putchar((n & mask) ?'1' :'0'); }}intmain(int argc,char **argv){if(argc !=2) { fprintf(stderr,"Usage: %s n\n", argv[0]);return1; } print_binary(atoi(argv[1])); putchar('\n');return0;}In the other direction, we can set thei-th bit ofx to 1 by doingx | (1 << i) or to 0 by doingx & ~(1 << i). See the section onbit manipulation. for applications of this to build arbitrarily-large bit vectors.
To add to the confusion, there are also threelogical operators that work on thetruth-values of integers, where 0 is defined to be false and anything else is defined by be true. These are&& (logical AND),||, (logical OR), and! (logical NOT). The result of any of these operators is always 0 or 1 (so!!x, for example, is 0 ifx is 0 and 1 ifx is anything else). The&& and|| operators evaluate their arguments left-to-right and ignore the second argument if the first determines the answer (this is the only place in C where argument evaluation order is specified); so
is in a very weak sense perfectly safe code to run.
Watch out for confusing& with&&. The expression1 & 2 evaluates to 0, but1 && 2 evaluates to 1. The statement0 & executeProgrammer(); is also unlikely to do what you want.
Yet another logical operator is theternary operator?:, wherex ? y : z equals the value ofy ifx is nonzero andz ifx is zero. Like&& and||, it only evaluates the arguments it needs to:
Most uses of?: are better done using anif-then-else statement.
The convention that Boolean values in C are represented by integers means that C traditionally did not have an explicit Boolean type. If you want to use explicit Boolean types, you can include thestdbool.h header file (added in C99) with#include <stdbool.h>. This doesn’t give you much: it makesbool an integer type that can hold Boolean values, and definesfalse andtrue to be constants 0 and 1. Sincebool is just another integer type, nothing prevents you from writingx = 12 / true or similar insults to the type system. But having explicitbool,false, andtrue keywords might make the intent of your code more explicit than the olderint/0/1 approach.
Logical operators usually operate on the results ofrelational operators or comparisons: these are== (equality),!= (inequality),< (less than),> (greater than),<= (less than or equal to) and>= (greater than or equal to). So, for example,
tests ifsize is in the (inclusive) range [MIN_SIZE..MAX_SIZE].
Beware of confusing== with=. The code
is perfectly legal C, and will setx to 5 rather than testing if it’s equal to 5. Because 5 happens to be nonzero, the body of theif statement will always be executed. This error is so common and so dangerous thatgcc will warn you about any tests that look like this if you use the-Wall option. Some programmers will go so far as to write the test as5 == x just so that if their finger slips, they will get a syntax error on5 = x even without special compiler support.
To input or output integer values, you will need to convert them from or to strings. Converting from a string is easy using theatoi oratol functions declared instdlib.h; these take a string as an argument and return anint orlong, respectively. C99 also providesatoll for converting tolong long. These routines have no ability to signal an error other than returning 0, so if you doatoi("Sweden"), 0 is what you will get.
Output is usually done usingprintf (orsprintf if you want to write to a string without producing output). Use the%d format specifier forints,shorts, andchars that you want the numeric value of,%ld forlongs, and%lld forlong longs.
A contrived program that uses all of these features is given below:
#include<stdio.h>#include<stdlib.h>/* This program can be used to show how atoi etc. handle overflow. *//* For example, try "overflow 1000000000000". */intmain(int argc,char **argv){char c;int i;long l;longlong ll;if(argc !=2) { fprintf(stderr,"Usage: %s n\n", argv[0]);return1; } c = atoi(argv[1]); i = atoi(argv[1]); l = atol(argv[1]); ll = atoll(argv[1]); printf("char: %d int: %d long: %ld long long: %lld", c, i, l, ll);return0;}Real numbers are represented in C by thefloating point typesfloat,double, andlong double. Just as the integer types can’t represent all integers because they fit in a bounded number of bytes, so also the floating-point types can’t represent all real numbers. The difference is that the integer types can represent values within their range exactly, while floating-point types almost always give only an approximation to the correct value, albeit across a much larger range. The three floating point types differ in how much space they use (32, 64, or 80 bits on x86 CPUs; possibly different amounts on other machines), and thus how much precision they provide. Most math library routines expect and returndoubles (e.g.,sin is declared asdouble sin(double), but there are usuallyfloat versions as well (float sinf(float)).
The core idea of floating-point representations (as opposed tofixed point representations as used by, say,ints), is that a numberx is written asm ⋅ be wherem is amantissa or fractional part,b is abase, ande is anexponent. On modern computers the base is almost always2, and for most floating-point representations the mantissa will be scaled to be between1 andb. This is done by adjusting the exponent, e.g.
| 1 = 1 ⋅ 20 |
| 2 = 1 ⋅ 21 |
| 0.375 = 1.5 ⋅ 2 − 2 |
etc.
The mantissa is usually represented in base2, as a binary fraction. So (in a very low-precision format), $1 would be1.000 ⋅ 20,2 would be1.000 ⋅ 21, and0.375 = 3/8 would be1.100 ⋅ 2 − 2, where the first1 after the decimal point counts as1/2, the second as1/4, etc. Note that for a properly-scaled (ornormalized) floating-point number in base2 the digit before the decimal point is always1. For this reason it is usually dropped to save space (although this requires a special representation for0).
Negative values are typically handled by adding asign bit that is0 for positive numbers and1 for negative numbers.
Any number that has a decimal point in it will be interpreted by the compiler as a floating-point number. Note that you have to put at least one digit after the decimal point:2.0,3.75,-12.6112. You can specific a floating point number in scientific notation usinge for the (base 10) exponent:6.022e23.
Floating-point types in C support most of the same arithmetic and relational operators as integer types;x > y,x / y,x + y all make sense whenx andy arefloats. If you mix two different floating-point types together, the less-precise one will be extended to match the precision of the more-precise one; this also works if you mix integer and floating point types as in2 / 3.0. Unlike integer division, floating-point division does not discard the fractional part (although it may produce round-off error:2.0/3.0 gives0.66666666666666663, which is not quite exact). Be careful about accidentally using integer division when you mean to use floating-point division:2/3 is0. Casts can be used to convert integer values to floating-point values to force floating-point division (see below).
Some operators that work on integers willnot work on floating-point types. These are% (usefmod from the math library if you really need to get a floating-point remainder) and all of the bitwise operators~,<<,>>,&,^, and|.
Mixed uses of floating-point and integer types will convert the integers to floating-point. So1 / 2.0 will give0.5.
You can convert floating-point numbers to and from integer types explicitly using casts. A typical use might be:
/* return the average of a list */doubleaverage(int n,int a[]){int sum =0;int i;for(i =0; i < n; i++) { sum += a[i]; }return (double) sum / n;}If we didn’t put in the(double) to convertsum to adouble, we’d end up doing integer division, which would truncate the fractional part of our average. Note that casts bind tighter than arithmetic operations, so the(double) applies to justsum, and not the whole expressionsum / n.
In the other direction, we can write:
to convert afloat f toint i. This conversion loses information by throwing away the fractional part off: iff was3.2,i will end up being just3.
The math library contains a pile of functions for converting values of typedouble to integer values of typedouble that give more control over the rounding: see for example the descriptions offloor,ceil,round,trunc, andnearbyint in theGNU libc reference manual.
C does not require a specific implementation of floating-point numbers, but unless you are running on very old or very odd hardware, you will probably find thatfloat,double, andlong double types are represented using theIEEE-754 floating-point standard. The standard is designed so that floating-point operations generally behave the way one would expect. This means that for most applications, you don’t need to know the details of the representation, but it is worth knowing about limits on accuracy of results, and about some extra floating-point values provided by IEEE-754 likeNaN (“not a number”) orinf (positive infinity).
IEEE-754 defines several standard representations of floating-point numbers, all of which have the following basic pattern (the specific layout here is for a 32-bitfloat):
bit 31 30 23 22 0 S EEEEEEEE MMMMMMMMMMMMMMMMMMMMMMMThe bit numbers are counting from the least-significant bit. The first bit is the sign (0 for positive, 1 for negative). The following 8 bits are the exponent inexcess-127 binary notation; this means that the binary pattern 01111111 = 127 represents an exponent of 0, 10000000 = 128 represents 1, 01111110 = 126 represents -1, and so forth. The mantissa fits in the remaining 24 bits, with its leading 1 stripped off as described above.
Certain numbers have a special representation. Because 0 cannot be represented in the standard form (there is no 1 before the decimal point), it is given the special representation0 00000000 00000000000000000000000. (There is also a -0 =1 00000000 00000000000000000000000, which looks equal to +0 but prints differently.) Numbers with exponents of 11111111 = 255 =2128 represent non-numeric quantities such as “not a number” (NaN), returned by operations like (0.0/0.0) and positive or negative infinity. A table of some typical floating-point numbers (generated by the programfloat.c) is given below:
0 = 0 = 0 00000000 00000000000000000000000 -0 = -0 = 1 00000000 00000000000000000000000 0.125 = 0.125 = 0 01111100 00000000000000000000000 0.25 = 0.25 = 0 01111101 00000000000000000000000 0.5 = 0.5 = 0 01111110 00000000000000000000000 1 = 1 = 0 01111111 00000000000000000000000 2 = 2 = 0 10000000 00000000000000000000000 4 = 4 = 0 10000001 00000000000000000000000 8 = 8 = 0 10000010 00000000000000000000000 0.375 = 0.375 = 0 01111101 10000000000000000000000 0.75 = 0.75 = 0 01111110 10000000000000000000000 1.5 = 1.5 = 0 01111111 10000000000000000000000 3 = 3 = 0 10000000 10000000000000000000000 6 = 6 = 0 10000001 10000000000000000000000 0.1 = 0.10000000149011612 = 0 01111011 10011001100110011001101 0.2 = 0.20000000298023224 = 0 01111100 10011001100110011001101 0.4 = 0.40000000596046448 = 0 01111101 10011001100110011001101 0.8 = 0.80000001192092896 = 0 01111110 10011001100110011001101 1e+12 = 999999995904 = 0 10100110 11010001101010010100101 1e+24 = 1.0000000138484279e+24 = 0 11001110 10100111100001000011100 1e+36 = 9.9999996169031625e+35 = 0 11110110 10000001001011111001110 inf = inf = 0 11111111 00000000000000000000000 -inf = -inf = 1 11111111 00000000000000000000000 nan = nan = 0 11111111 10000000000000000000000What this means in practice is that a 32-bit floating-point value (e.g. afloat) can represent any number between1.17549435e-38 and3.40282347e+38, where thee separates the (base 10) exponent. Operations that would create a smaller value will underflow to 0 (slowly—IEEE 754 allows “denormalized” floating point numbers with reduced precision for very small values) and operations that would create a larger value will produceinf or-inf instead.
For a 64-bitdouble, the size of both the exponent and mantissa are larger; this gives a range from1.7976931348623157e+308 to2.2250738585072014e-308, with similar behavior on underflow and overflow.
Intel processors internally use an even larger 80-bit floating-point format for all operations. Unless you declare your variables aslong double, this should not be visible to you from C except that some operations that might otherwise produce overflow errors will not do so, provided all the variables involved sit in registers (typically the case only for local variables and function parameters).
In general, floating-point numbers are not exact: they are likely to containround-off error because of the truncation of the mantissa to a fixed number of bits. This is particularly noticeable for large values (e.g. 1e+12 in the table above), but can also be seen in fractions with values that aren’t powers of 2 in the denominator (e.g.0.1). Round-off error is often invisible with the default float output formats, since they produce fewer digits than are stored internally, but can accumulate over time, particularly if you subtract floating-point quantities with values that are close (this wipes out the mantissa without wiping out the error, making the error much larger relative to the number that remains).
The easiest way to avoid accumulating error is to use high-precision floating-point numbers (this means usingdouble instead offloat). On modern CPUs there is little or no time penalty for doing so, although storingdoubles instead offloats will take twice as much space in memory.
Note that a consequence of the internal structure of IEEE 754 floating-point numbers is that small integers and fractions with small numerators and power-of-2 denominators can be representedexactly—indeed, the IEEE 754 standard carefully defines floating-point operations so that arithmetic on such exact integers will give the same answers as integer arithmetic would (except, of course, for division that produces a remainder). This fact can sometimes be exploited to get higher precision on integer values than is available from the standard integer types; for example, adouble can represent any integer between − 253 and253 exactly, which is a much wider range than the values from2 − 31 to 231 − 1 that fit in a 32-bit int or long. (A 64-bit long long does better.) So double should be considered for applications where large precise integers are needed (such as calculating the net worth in pennies of a billionaire.)
One consequence of round-off error is that it is very difficult to test floating-point numbers for equality, unless you are sure you have an exact value as described above. It is generally not the case, for example, that(0.1+0.1+0.1) == 0.3 in C. This can produce odd results if you try writing something likefor(f = 0.0; f <= 0.3; f += 0.1): it will be hard to predict in advance whether the loop body will be executed withf = 0.3 or not. (Even more hilarity ensues if you writefor(f = 0.0; f != 0.3; f += 0.1), which after not quite hitting0.3 exactly keeps looping for much longer than I am willing to wait to see it stop, but which I suspect will eventually converge to some constant value off large enough that adding0.1 to it has no effect.) Most of the time when you are tempted to test floats for equality, you are better off testing if one lies within a small distance from the other, e.g. by testingfabs(x-y) <= fabs(EPSILON * y), whereEPSILON is usually some application-dependent tolerance. This isn’t quite the same as equality (for example, it isn’t transitive), but it usually closer to what you want.
Any numeric constant in a C program that contains a decimal point is treated as adouble by default. You can also usee orE to add a base-10 exponent (see the table for some examples of this.) If you want to insist that a constant value is afloat for some reason, you can appendF on the end, as in1.0F.
For I/O, floating-point values are most easily read and written usingscanf (and its relativesfscanf andsscanf) andprintf. Forprintf, there is an elaborate variety of floating-point format codes; the easiest way to find out what these do is experiment with them. Forscanf, pretty much the only two codes you need are"%lf", which reads adouble value into adouble *, and"%f", which reads afloat value into afloat *. Both these formats are exactly the same inprintf, since afloat is promoted to adouble before being passed as an argument toprintf (or any other function that doesn’t declare the type of its arguments). But you have to be careful with the arguments toscanf or you will get odd results as only 4 bytes of your 8-bytedouble are filled in, or—even worse—8 bytes of your 4-bytefloat are.
The valuesnan,inf, and-inf can’t be written in this form as floating-point constants in a C program, butprintf will generate them andscanf seems to recognize them. With some machines and compilers you may be able to use the macrosINFINITY andNAN from<math.h> to generate non-finite quantities. The macrosisinf andisnan can be used to detect such quantities if they occur.
(See also K&R Appendix B4.)
Many mathematical functions on floating-point values are not linked into C programs by default, but can be obtained by linking in the math library. Examples would be the trigonometric functionssin,cos, andtan (plus more exotic ones),sqrt for taking square roots,pow for exponentiation,log andexp for base-e logs and exponents, andfmod for when you really want to writex%y but one or both variables is adouble. The standard math library functions all takedoubles as arguments and returndouble values; most implementations also provide some extra functions with similar names (e.g.,sinf) that usefloats instead, for applications where space or speed is more important than accuracy.
There are two parts to using the math library. The first is to include the line
somewhere at the top of your source file. This tells the preprocessor to paste in the declarations of the math library functions found in/usr/include/math.h.
The second step is to link to the math library when you compile. This is done by passing the flag-lm togccafter your C program source file(s). A typical command might be:
gcc -o program program.c -lmIf you don’t do this, you will get errors from the compiler about missing functions. The reason is that the math library is not linked in by default, since for many system programs it’s not needed.
Operator precedence in C controls the interpretation of ambiguous expressions like2+3*4, which could in principle be parsed either as2+(3*4) (the right way) or as(2+3)*4 (the cheap calculator way). For the most part, C parses unparenthesized expressions the right way, but if you are not sure what it will do with an expression, you can always put in parentheses to force it to do the right thing.
There is a table on page 53 of Kernighan and Ritchie that shows the precedence of all operators in C, which we reproduce below.
The interpretation of this table is that higher entries bind tighter than lower ones; so the fact that* has higher precedence that+ and both have higher precedence than> means that2+3*4 > 5 gets parsed as(2+(3*4)) > 5.
Associativity controls how an expression with multiple operators of the same precedence is interpreted. The fact that+ and- associate left-to-right means that the expression2+3-4-5 is interpreted as(((2+3)-4)-5): the leftmost operation is done first. Unary operators, ternary?: and assignment operators are the only ones that associate right-to-left. For assignment operators, this is sox = y = 0 is interpreted asx = (y = 0) (assigning0 to bothx andy) and not(x = y) = 0 (which would give an error because(x = y) isn’t something you can assign to). For unary operators, this mostly affects expressions like*p++, which is equivalent to*(p++) (increment the pointer first then dereference it) rather than(*p)++ (increment the thing thatp points to).
()[]->. | function calls and indexing |
!~- (unary)* (unary)&(unary)++--(type)sizeof | unary operators (associate right-to-left) |
* (binary)/% | multiplication and division |
+ (binary)- (binary) | addition and subtraction |
<<>> | shifts |
<<=>=> | inequalities |
==!= | equality |
& (binary) | bitwise AND |
^ | bitwise XOR |
| | bitwise OR |
&& | logical AND |
|| | logical OR |
?: | ternary if (associates right-to-left) |
=+=-=*=/=%=&=^=|=<<=>>= | assignment (associate right-to-left) |
, | comma |
The C programming language imposes very few constraints on how programs are formatted and organized. Both of the following are legitimate C programs, which compile to exactly the same machine code usinggcc with a high enough optimization level:
/* * Count down from COUNTDOWN_START (defined below) to 0. * Prints all numbers in the range including both endpoints. */#include<stdio.h>#define COUNTDOWN_START (10)intmain(int argc,char **argv){for(int i = COUNTDOWN_START; i >=0; i--) { printf("%d\n", i); }return0;}The difference between these programs is that the first is designed to be easy to read and understand while the second is not. Though the compiler can’t tell the difference between them, the second will be much harder to debug or modify to accomplish some new task.
Certain formatting and programming conventions have evolved over the years to make C code as comprehensible as possible, and as we introduce various features of C, we will talk about how best to use them to make your programs understood by both computers and humans.
Submitted assignments may be graded for style in addition to correctness. Below is a checklist that has been used in past versions of the course to identify some of the more egregious violations of reasonable coding practice. For more extreme examples of what not to do, see theInternational Obfuscated C Code Contest.
Style grading checklistScore is 20 points minus 1 for each box checked (but never less than 0)Comments[ ] Undocumented module.[ ] Undocumented function other than main.[ ] Underdocumented function: return value or args not described.[ ] Undocumented program input and output (when main is provided).[ ] Undocumented struct or union components.[ ] Undocumented #define.[ ] Failure to cite code taken from other sources.[ ] Insufficient comments.[ ] Excessive comments.Naming[ ] Meaningless function name.[ ] Confusing variable name.[ ] Inconsistent variable naming style (UgLyName, ugly_name, NAME___UGLY_1).[ ] Inconsistent use of capitalization to distinguish constants.Whitespace[ ] Inconsistent or misleading indentation.[ ] Spaces not used or used misleadingly to break up complicated expressions.[ ] Blank lines not used or used misleadingly to break up long function bodies.Macros[ ] Non-trivial constant with no symbolic name.[ ] Failure to parenthesize expression in macro definition.[ ] Dependent constant not written as expression of earlier constant.[ ] Underdocumented parameterized macro.Global variables[ ] Inappropriate use of a global variable.Functions[ ] Kitchen-sink function that performs multiple unrelated tasks.[ ] Non-void function that returns no useful value.[ ] Function with too many arguments.Code organization[ ] Lack of modularity.[ ] Function used in multiple source files but not declared in header file.[ ] Internal-use-only function not declared static.[ ] Full struct definition in header files when components should be hidden.[ ] #include "file.c"[ ] Substantial repetition of code.Miscellaneous[ ] Other obstacle to readability not mentioned above.Variables in C are a direct abstraction of physical memory locations. To understand how variables work, it helps to start by understanding how computer memory works.
Memory consists of many bytes of storage, each of which has an address which is itself a sequence of bits. Though the actual memory architecture of a modern computer is complex, from the point of view of a C program we can think of as simply a largeaddress space that the CPU can store things in (and load things from), provided it can supply an address to the memory. Because we don’t want to have to type long strings of bits all the time, the C compiler lets us give names to particular regions of the address space, and will even find free space for us to use.
Avariable is a name given in a program for some region of memory. Each variable has atype, which tells the compiler how big the region of memory corresponding to it is and how to treat the bits stored in that region when performing various kinds of operations (e.g. integer variables are added together by very different circuitry than floating-point variables, even though both represent numbers as bits). In modern programming languages, a variable also has ascope (a limit on where the name is meaningful, which allows the same name to be used for different variables in different parts of the program) and anextent (the duration of the variable’s existence, controlling when the program allocates and deallocates space for it).
Before you can use a variable in C, you mustdeclare it. Variable declarations show up in three places:
for loops).Another feature of function parameters and local variables is that if a function is called more than once (even if the functioncalls itself), each copy of the function gets its own local variables.
Variable declarations consist of a type name followed by one or more variable names separated by commas and terminated by a semicolon (except in argument lists, where each declaration is terminated by a comma). I personally find it easiest to declare variables one per line, to simplify documenting them. It is also possible for global and local variables (but not function arguments) to assign an initial value to a variable by putting in something like= 0 after the variable name. It is good practice to put a comment after each variable declaration that explains what the variable does (with a possible exception for conventionally-named loop variables likei orj in short functions). Below is an example of a program with some variable declarations in it:
#include<stdio.h>#include<ctype.h>/* This program counts the number of digits in its input. *//* *This global variable is not used; it is here only to demonstrate * what a global variable declaration looks like. */unsignedlong SpuriousGlobalVariable =127;intmain(int argc,char **argv){int c;/* character read */int count =0;/* number of digits found */while((c = getchar()) != EOF) {if(isdigit(c)) { count++; } } printf("%d\n", count);return0;}C is pretty generous about what you can use in a variable name: generally any sequence of digits, letters, and the underscore character_ can be used, so long as the first character is not a digit. Very old versions of C may limit the length ofexternal variables (those that can be reference from other files, like library routines) to 6 characters, but modern versions don’t. (This explains the compact form of many standard library routine names likemalloc,printf, orstrlen.)
Older languages were more restrictive, and variable names have evolved over the years:
#FC27A1$IFNXG7I at the start means it’s an integer variable. The rest of the letters probably abbreviate some much longer description of what the variable means. The default type based on the first letter was used because FORTRAN programmers were lazy, but it could be overridden by an explicit declaration.i,j,c,count,top_of_stack,accumulatedTimeInFlightprgcGradeDatabaseAn example ofHungarian notation, a style of variable naming in which the type of the variable is encoded in the first few character. The type is now back in the variable name again. This isnot enforced by the compiler: even thoughiNumberOfStudents is supposed to be anint, there is nothing to prevent you from declaringfloat iNumberOfStudents if you are teaching a class on improper chainsaw handling and want to allow for the possibility of fractional students. Seethis MSDN page for a much more detailed explanation of the system.
Not clearly an improvement on standard naming conventions, but it is popular in some programming shops.
In C, variable names are calledidentifiers. These are also used to identify things that are not variables, like functions and user-defined types.
An identifier in C must start with a lower or uppercase letter or the underscore character_. Typically variables starting with underscores are used internally by system libraries, so it’s dangerous to name your own variables this way. Subsequent characters in an identifier can be letters, digits, or underscores. So for examplea,____a___a_a_11727_a,AlbertEinstein,aAaAaAaAaAAAAAa, and______ are all legal identifiers in C, but$foo and01 are not.
The basic principle of variable naming is that a variable name is a substitute for the programmer’s memory. It is generally best to give identifiers names that are easy to read and describe what the variable is used for. Such variables are calledself-documenting. None of the variable names in the preceding list are any good by this standard. Better names would betotal_input_characters,dialedWrongNumber, orstepsRemaining. Non-descriptive single-character names are acceptable for certain conventional uses, such as the use ofi andj for loop iteration variables, orc for an input character. Such names should only be used when the scope of the variable is small, so that it’s easy to see all the places where it is used at the same time.
C identifiers are case-sensitive, soaardvark,AArDvARK, andAARDVARK are all different variables. Because it is hard to remember how you capitalized something before, it is important to pick a standard convention and stick to it. The traditional convention in C goes like this:
count,countOfInputBits.Stack,TotalBytesAllocated.#define orenum are written in all-caps:MAXIMUM_STACK_SIZE,BUFFER_LIMIT.Ignoringpointers for the moment, there are essentially two things you can do to a variable. You can assign a value to it using the= operator, as in:
or you can use its value in an expression:
The assignment operator is an ordinary operator, and assignment expressions can be used in larger expressions:
This feature is usually only used in certain standard idioms, since it’s confusing otherwise.
There are also shorthand operators for expressions of the formvariable=variableoperatorexpression. For example, writingx += y is equivalent to writingx = x + y,x /= y is the same asx = x / y, etc.
For the special case of adding or subtracting 1, you can abbreviate still further with the++ and-- operators. These come in two versions, depending on whether you want the result of the expression (if used in a larger expression) to be the value of the variable before or after the variable is incremented:
x =0; y = x++;/* sets x to 1 and y to 0 (the old value) */ y = ++x;/* sets x to 2 and y to 2 (the new value) */ y = x--;/* sets x to 1 and y to 2 (the old value) */ y = --x;/* sets x to 0 and y to 0 (the new value) */The intuition is that if the++ comes before the variable, the increment happens before the value of the variable is read (apreincrement; if it comes after, it happens after the value is read (apostincrement). This is confusing enough that it is best not to use the value of preincrement or postincrement operations except in certain standard idioms. But usingx++ or++x by itself as a substitute forx = x+1 is perfectly acceptable style.8
It is a serious error to use the value of a variable that has never been assigned to, because you will get whatever junk is sitting in memory at the address allocated to the variable, and this might be some arbitrary leftover value from a previous function call that doesn’t even represent the same type.9
Fortunately, C provides a way to guarantee that a variable is initialized as soon as it is declared. Many of the examples in the notes do not use this mechanism, because of bad habits learned by the instructor using early versions of C that imposed tighter constraints on initialization. But initializing variables is a good habit to get in the practice of doing.
For variables with simple types (that is, notarrays,structs, orunions), an initializer looks like an assignment:
int sum =0;int n =100;int nSquared = n*n;double gradeSchoolPi =3.14;constchar *const greeting ="Hi!";constint greetingLength = strlen(greeting);For ordinary local variables, the initializer value can be any expression, including expressions that call other functions. There is an exception for variables allocated when the program starts (which includes global variables outside functions andstatic variables inside functions), which can only be initialized to constant expressions.
The last two examples show how initializers can set the values of variables that are declared to beconst (the variablegreeting is both constant itself, because ofconst greeting, and points to data that is also constant, because it is of typeconst char). This is the only way to set the values of such variables without cheating, because the compiler will complain if you try to do an ordinary assignment to a variable declared to be constant.
For fixed-sizearrays andstructs, it is possible to supply an initializer for each component, by enclosing the initializer values in braces, separated by commas. For example:
int threeNumbers[3] = {1,2,3 };struct numericTitle {int number;constchar *name; };struct numericTitle s = {7,"Samurai" };struct numericTitle n = {3,"Ninjas" };It is possible to specify additional information about how a variable can be used usingstorage class qualifiers, which usually go before the type of a variable in a declaration.
Most variables that you will use in C are either parameters tofunctions or local variables inside functions. These havelocal scope, meaning the variable names can only be used in the function in which they are declared, andautomatic extent, meaning the space for the variable is allocated, typically on the stack, when the function is called, and reclaimed when the function exits. (If the function calls itself, you get another copy of all the local variables; seerecursion.)
Onvery rare occasions you might want to have a variable that survives the entire execution of a program (hasstatic extent) or that is visible throughout the program (hasglobal scope). C provides a mechanism for doing thisthat you shold never use under normal circumstances. Pretty much the only time you are going to want to have a variable with static extent is if you are keeping track of some piece of information that (a) you only need one instance of, (b) you need to survive between function calls, and (c) it would be annoying to pass around as an extra argument to any function that uses it. An example would be the internal data structures used bymalloc, or the count variable in the function below:
/* returns the number of times this function has previously been called *//* this can be used to generate unique numerical identifiers */unsignedlonglongticketMachine(void){staticunsignedlonglong count =0;return count++;}To declare a local variable with static extent, use thestatic qualifier as in the above example. To declare a global variable with static extent, declare it outside a function. In both cases you should provide aninitializer for the variable.
It is possible to put some additional constraints on the visibility of global variables. By default, a global variable will be visible everywhere, but functions files other than the one in which it is defined won’t necessarily know what type it has. This latter problem can be fixed using anextern declaration, which says that there is a variable somewhere else of a particular type that we are declaring (but not defining, so no space is allocated). In contrast, thestatic keyword (on a global variable) specifies that it will only be visible in the current file, even if some other file includes a declaration of a global variable of the same name.
Here are three variable declarations that illustrate how this works:
unsignedshort Global =5;/* global variable, can be used anywhere */externfloat GlobalFloat;/* this global variable, defined somewhere else, has type float */staticchar Character ='c';/* global variable, can only be used by functions in this file */(Note the convention of putting capital letters on global variables to distinguish them from local variables.)
Typically, anextern definition would appear in a header file so that it can be included in any function that uses the variable, while an ordinary global variable definition would appear in a C file so it only occurs once.
Theconst qualifier declares a variable to be constant:
It is an error to apply any sort of assignment (=,+=,++, etc.) to a variable qualified asconst.
constApointer to a region that should not be modified should be declared withconst type:
Theconst in the declaration above applies to the characters thatstring points to:string is notconst itself, but is instead apointer toconst. It is still possible to makestring point somewhere else, say by doing an assignment:
If you want to make it so that you can’t assign tostring, putconst right before the variable name:
Nowstring is aconst pointer toconst: you can neither modifystring nor the values it points to.
Note thatconst only restricts what you can do using this particular variable name. If you can get at the memory that something points to by some other means, say through another pointer, you may be able to change the values in these memory locations anyway:
int x =5;constint *p = &x;int *q; *p =1;/* will cause an error at compile time */ x =3;/* also changes *p, but will not cause an error */Input and output from C programs is typically done through thestandard I/O library, whose functions etc. are declared instdio.h. A detailed descriptions of the functions in this library is given in Appendix B of Kernighan and Ritchie. We’ll talk about some of the more useful functions and about how input-output (I/O) works on Unix-like operating systems in general.
The standard I/O library works oncharacter streams, objects that act like long sequences of incoming or outgoing characters. What a stream is connected to is often not apparent to a program that uses it; an output stream might go to a terminal, to a file, or even to another program (appearing there as an input stream).
Three standard streams are available to all programs: these arestdin (standard input),stdout (standard output), andstderr (standard error). Standard I/O functions that do not take a stream as an argument will generally either read fromstdin or write tostdout. Thestderr stream is used for error messages. It is kept separate fromstdout so that you can see these messages even if you redirect output to a file:
$ ls no-such-file > /tmp/outputls: no-such-file: No such file or directoryTo read a single character fromstdin, usegetchar:
Thegetchar routine will return the special valueEOF (usually -1; short forend of file) if there are no more characters to read, which can happen when you hit the end of a file or when the user types the end-of-file key control-D to the terminal. Note that the return value ofgetchar is declared to be anint sinceEOF lies outside the normal character range.
To write a single character tostdout, useputchar:
Even thoughputchar can only write single bytes, it takes anint as an argument. Any value outside the range 0..255 will be truncated to its last byte, as in the usual conversion fromint tounsigned char.
Bothgetchar andputchar are wrappers for more general routinesgetc andputc that allow you to specify which stream you are using. To illustrategetc andputc, here’s how we might definegetchar andputchar if they didn’t exist already:
Note thatputc,putchar2 as defined above, and the originalputchar all return anint rather thanvoid; this is so that they can signal whether the write succeeded. If the write succeeded,putchar orputc will return the value written. If the write failed (say because the disk was full), thenputc orputchar will returnEOF.
Here’s another example of usingputc to make a new functionputcerr that writes a character tostderr:
A rather odd feature of the C standard I/O library is that if you don’t like the character you just got, you can put it back using theungetc function. The limitations onungetc are that (a) you can only push one character back, and (b) that character can’t beEOF. Theungetc function is provided because it makes certain high-level input tasks easier; for example, if you want to parse a number written as a sequence of digits, you need to be able to read characters until you hit the first non-digit. But if the non-digit is going to be used elsewhere in your program, you don’t want to eat it. The solution is to put it back usingungetc.
Here’s a function that usesungetc to peek at the next character onstdin without consuming it:
/* return the next character from stdin without consuming it */intpeekchar(void){int c; c = getchar();if(c != EOF) ungetc(c, stdin);/* puts it back */return c;}Reading and writing data one character at a time can be painful. The C standard I/O library provides several convenient routines for reading and writing formatted data. The most commonly used one isprintf, which takes as arguments a format string followed by zero or more values that are filled in to the format string according to patterns appearing in it.
Here are some typicalprintf statements:
printf("Hello\n");/* print "Hello" followed by a newline */ printf("%c", c);/* equivalent to putchar(c) */ printf("%d", n);/* print n (an int) formatted in decimal */ printf("%u", n);/* print n (an unsigned int) formatted in decimal */ printf("%o", n);/* print n (an unsigned int) formatted in octal */ printf("%x", n);/* print n (an unsigned int) formatted in hexadecimal */ printf("%f", x);/* print x (a float or double) *//* print total (an int) and average (a double) on two lines with labels */ printf("Total: %d\nAverage: %f\n", total, average);For a full list of formatting codes see Table B-1 in Kernighan and Ritchie, or runman 3 printf.
The inverse ofprintf isscanf. Thescanf function reads formatted data fromstdin according to the format string passed as its first argument and stuffs the results into variables whoseaddresses are given by the later arguments. This requires prefixing each such argument with the& operator, which takes the address of a variable.
Format strings forscanf are close enough to format strings forprintf that you can usually copy them over directly. However, becausescanf arguments don’t go through argument promotion (where all small integer types are converted toint andfloats are converted todouble), you have to be much more careful about specifying the type of the argument correctly. For example, whileprintf("%f", x) will work whetherx is afloat or adouble,scanf("%f", &x) will work only ifx is afloat, which means thatscanf("%lf", &x) is needed ifx is in fact adouble.
Some examples:
scanf("%c", &c);/* like c = getchar(); c must be a char; will NOT put EOF in c */ scanf("%d", &n);/* read an int formatted in decimal */ scanf("%u", &n);/* read an unsigned int formatted in decimal */ scanf("%o", &n);/* read an unsigned int formatted in octal */ scanf("%x", &n);/* read an unsigned int formatted in hexadecimal */ scanf("%f", &x);/* read a float */ scanf("%lf", &x);/* read a double *//* read total (an int) and average (a float) on two lines with labels *//* (will also work if input is missing newlines or uses other whitespace, see below) */ scanf("Total: %d\nAverage: %f\n", &total, &average);For a full list of formatting codes, runman 3 scanf.
Thescanf routine usually eats whitespace (spaces, tabs, newlines, etc.) in its input whenever it sees a conversion specification or a whitespace character in its format string. The one exception is that a%c conversion specifier will not eat whitespace and will instead return the next character whether it is whitespace or not. Non-whitespace characters that are not part of conversion specifications must match exactly. To detect ifscanf parsed everything successfully, look at its return value; it returns the number of values it filled in, orEOF if it hits end-of-file before filling in any values.
Theprintf andscanf routines are wrappers forfprintf andfscanf, which take a stream as their first argument, e.g.:
This sends the output to the standard error output handlestderr. Note the use of “%%” to print a single percent in the output.
Since we can write our own functions in C, if we don’t like what the standard routines do, we can build our own on top of them. For example, here’s a function that reads in integer values without leading minus signs and returns the result. It uses thepeekchar routine we defined above, as well as theisdigit routine declared inctype.h.
/* read an integer written in decimal notation from stdin until the first * non-digit and return it. Returns 0 if there are no digits. */intreadNumber(void){int accumulator;/* the number so far */int c;/* next character */ accumulator =0;while((c = peekchar()) != EOF && isdigit(c)) { c = getchar();/* consume it */ accumulator *=10;/* shift previous digits over */ accumulator += (c -'0');/* add decimal value of new digit */ }return accumulator;}Here’s another implementation that doesalmost the same thing:
The difference is thatreadNumber2 will consume any whitespace before the first digit, which may or may not be what we want.
More complex routines can be used to parse more complex input. For example, here’s a routine that usesreadNumber to parse simple arithmetic expressions, where each expression is either a number or of the form(expression+expression) or(expression*expression). The return value is the value of the expression after adding together or multiplying all of its subexpressions. (A complete program including this routine and the others defined earlier that it uses can be foundexamples/IO/calc.c.
#define EXPRESSION_ERROR (-1)/* read an expression from stdin and return its value *//* returns EXPRESSION_ERROR on error */intreadExpression(void){int e1;/* value of first sub-expression */int e2;/* value of second sub-expression */int c;int op;/* operation: '+' or '*' */ c = peekchar();if(c =='(') { c = getchar(); e1 = readExpression(); op = getchar(); e2 = readExpression(); c = getchar();/* this had better be ')' */if(c !=')')return EXPRESSION_ERROR;/* else */switch(op) {case'*':return e1*e2;break;case'+':return e1+e2;break;default:return EXPRESSION_ERROR;break; } }elseif(isdigit(c)) {return readNumber(); }else {return EXPRESSION_ERROR; }}Because this routine calls itself recursively as it works its way down through the input, it is an example of arecursive descent parser. Parsers for more complicated languages like C are usually not written by hand like this, but are instead constructed mechanically using aparser generator.
Reading and writing files is done by creating new streams attached to the files. The function that does this isfopen. It takes two arguments: a filename, and a flag that controls whether the file is opened for reading or writing. The return value offopen has typeFILE * and can be used inputc,getc,fprintf, etc. just likestdin,stdout, orstderr. When you are done using a stream, you should close it usingfclose.
Here’s a program that reads a list of numbers from a file whose name is given asargv[1] and prints their sum:
#include<stdio.h>#include<stdlib.h>/* * Sum integers in a file. * * 2018-01-24 Includes bug fixes contributed by Zhe Hua. */intmain(int argc,char **argv){FILE *f;int x;int sum;if(argc !=2) { fprintf(stderr,"Usage: %s filename\n", argv[0]); exit(1); } f = fopen(argv[1],"r");if(f ==0) {/* perror is a standard C library routine *//* that prints a message about the last failed library routine *//* prepended by its argument */ perror(argv[1]); exit(2); }/* else everything is ok */ sum =0;while(fscanf(f,"%d", &x) ==1) { sum += x; } printf("%d\n", sum);/* not strictly necessary but it's polite */ fclose(f);return0;}To write to a file, open it withfopen(filename, "w"). Note that as soon as you callfopen with the"w" flag, any previous contents of the file are erased. If you want to append to the end of an existing file, use"a" instead. You can also add+ onto the flag if you want to read and write the same file (this will probably involve usingfseek).
Some operating systems (Windows) make a distinction between text and binary files. For text files, use the same arguments as above. For binary files, add ab, e.g.fopen(filename, "wb") to write a binary file.
/* leave a greeting in the current directory */#include<stdio.h>#include<stdlib.h>#define FILENAME "hello.txt"#define MESSAGE "hello world"intmain(int argc,char **argv){FILE *f; f = fopen(FILENAME,"w");if(f ==0) { perror(FILENAME); exit(1); }/* unlike puts, fputs doesn't add a newline */ fputs(MESSAGE, f); putc('\n', f); fclose(f);return0;}The bodies of C functions (including themain function) are made up ofstatements. These can either besimple statements that do not contain other statements, orcompound statements that have other statements inside them.Control structures are compound statements like if/then/else, while, for, and do..while that control how or whether their component statements are executed.
The simplest kind of statement in C is an expression (followed by a semicolon, the terminator for all simple statements). Its value is computed and discarded. Examples:
x =2;/* an assignment statement */ x =2+3;/* another assignment statement */2+3;/* has no effect---will be discarded by smart compilers */ puts("hi");/* a statement containing a function call */ root2 = sqrt(2);/* an assignment statement with a function call */Most statements in a typical C program are simple statements of this form.
Other examples of simple statements are the jump statementsreturn,break,continue, andgoto. Areturn statement specifies the return value for a function (if there is one), and when executed it causes the function to exit immediately. Thebreak andcontinue statements jump immediately to the end of a loop (orswitch; see below) or the next iteration of a loop; we’ll talk about these more when we talk about loops. Thegoto statement jumps to another location in the same function, and exists for the rare occasions when it is needed. Using it in most circumstances is a sin.
Compound statements come in two varieties: conditionals and loops.
These are compound statements that test some condition and execute one or another block depending on the outcome of the condition. The simplest is theif statement:
Thebody of theif statement is executed only if the expression in parentheses at the top evaluates to true (which in C means any value that is not 0).
The braces are not strictly required, and are used only to group one or more statements into a single statement. If there is only one statement in the body, the braces can be omitted:
This style is recommended only for very simple bodies. Omitting the braces makes it harder to add more statements later without errors.
if(underAttack) launchCounterAttack();/* executed only when attacked */ hideInBunker();/*### DO NOT INDENT LIKE THIS### executed always */In the example above, the lack of braces means that thehideInBunker() statement isnot part of theif statement, despite the misleading indentation. This sort of thing is why I generally always put in braces in anif.
Anif statement may have anelse clause, whose body is executed if the test is false (i.e. equal to 0).
A common idiom is to have a chain ofif andelse if branches that test several conditions:
if(temperature <0) { puts("brrr"); }elseif(temperature <100) { puts("hooray"); }else { puts("ouch!"); }This can be inefficient if there are a lot of cases, since the tests are applied sequentially. For tests of the form<expression>==<small constant>, theswitch statement may provide a faster alternative. Here’s a typicalswitch statement:
/* print plural of cow, maybe using the obsolete dual number construction */switch(numberOfCows) {case1: puts("cow");break;case2: puts("cowen");break;default: puts("cows");break; }This prints the string “cow” if there is one cow, “cowen” if there are two cowen, and “cows” if there are any other number of cows. Theswitch statement evaluates its argument and jumps to the matchingcase label, or to thedefault label if none of the cases match. Cases must be constant integer values.
Thebreak statements inside the block jump to the end of the block. Without them, executing theswitch withnumberOfCows equal to 1 would print all three lines. This can be useful in some circumstances where the same code should be used for more than one case:
switch(c) {case'a':case'e':case'i':case'o':case'u': type = VOWEL;break;default: type = CONSONANT;break; }or when a case “falls through” to the next:
switch(countdownStart) {case3: puts("3");case2: puts("2");case1: puts("1")case0: puts("KABLOOIE!");break;default: puts("I can't count that high!");break; }Note that it is customary to include abreak on the last case even though it has no effect; this avoids problems later if a new case is added. It is also customary to include adefault case even if the other cases supposedly exhaust all the possible values, as a check against bad or unanticipated inputs.
switch(oliveSize) {case JUMBO: eatOlives(SLOWLY);break;case COLLOSSAL: eatOlives(QUICKLY);break;case SUPER_COLLOSSAL: eatOlives(ABSURDLY);break;default:/* unknown size! */ abort();break; }Thoughswitch statements are better than deeply nested if/else-if constructions, it is often even better to organize the different cases as data rather than code. We’ll see examples of this when we talk aboutfunction pointers.
Nothing in the C standards prevents thecase labels from being buried inside other compound statements. One rather hideous application of this fact isDuff’s device.
There are three kinds of loops in C.
Awhile loop tests if a condition is true, and if so, executes its body. It then tests the condition is true again, and keeps executing the body as long as it is. Here’s a program that deletes every occurrence of the lettere from its input.
#include<stdio.h>intmain(int argc,char **argv){int c;while((c = getchar()) != EOF) {switch(c) {case'e':case'E':break;default: putchar(c);break; } }return0;}Note that the expression inside thewhile argument both assigns the return value ofgetchar toc and tests to see if it is equal toEOF (which is returned when no more input characters are available). This is a very common idiom in C programs. Note also that even thoughc holds a single character, it is declared as anint. The reason is thatEOF (a constant defined instdio.h) is outside the normal character range, and if you assign it to a variable of typechar it will be quietly truncated into something else. Because C doesn’t provide any sort of exception mechanism for signalling unusual outcomes of function calls, designers of library functions often have to resort to extending the output of a function to include an extra value or two to signal failure; we’ll see this a lot when the null pointer shows up in the chapter onpointers.
Thedo..while statement is like thewhile statement except the test is done at the end of the loop instead of the beginning. This means that the body of the loop is always executed at least once.
Here’s a loop that does a random walk until it gets back to 0 (if ever). If we changed thedo..while loop to awhile loop, it would never take the first step, becausepos starts at 0.
#include<stdio.h>#include<stdlib.h>#include<time.h>intmain(int argc,char **argv){int pos =0;/* position of random walk */ srandom(time(0));/* initialize random number generator */do { pos += random() &0x1 ? +1 : -1; printf("%d\n", pos); }while(pos !=0);return0;}Thedo..while loop is used much less often in practice than thewhile loop.
It is theoretically possible to convert ado..while loop to awhile loop by making an extra copy of the body in front of the loop, but this is not recommended since it’s almost always a bad idea to duplicate code.
Thefor loop is a form ofsyntactic sugar that is used when a loop iterates over a sequence of values stored in some variable (or variables). Its argument consists of three expressions: the first initializes the variable and is called once when the statement is first reached. The second is the test to see if the body of the loop should be executed; it has the same function as the test in awhile loop. The third sets the variable to its next value. Some examples:
/* count from 0 to 9 */for(i =0; i <10; i++) { printf("%d\n", i); }/* and back from 10 to 0 */for(i =10; i >=0; i--) { printf("%d\n", i); }/* this loop uses some functions to move around */for(c = firstCustomer(); c != END_OF_CUSTOMERS; c = customerAfter(c)) { helpCustomer(c); }/* this loop prints powers of 2 that are less than n*/for(i =1; i < n; i *=2) { printf("%d\n", i); }/* this loop does the same thing with two variables by using the comma operator */for(i =0, power =1; power < n; i++, power *=2) { printf("2^%d = %d\n", i, power); }/* Here are some nested loops that print a times table */for(i =0; i < n; i++) {for(j =0; j < n; j++) { printf("%d*%d=%d ", i, j, i*j); } putchar('\n'); }Afor loop can always be rewritten as awhile loop.
for(i =0; i <10; i++) { printf("%d\n", i); }/* is exactly the same as */ i =0;while(i <10) { printf("%d\n", i); i++; }Thebreak statement immediately exits the innermmost enclosing loop orswitch statement.
Thecontinue statement skips to the next iteration. Here is a program with a loop that iterates through all the integers from -10 through 10, skipping 0:
#include<stdio.h>/* print a table of inverses */#define MAXN (10)intmain(int argc,char **argv){int n;for(n = -MAXN; n <= MAXN; n++) {if(n ==0)continue; printf("1.0/%3d = %+f\n", n,1.0/n); }return0;}Occasionally, one would like to break out of more than one nested loop. The way to do this is with agoto statement.
for(i =0; i < n; i++) {for(j =0; j < n; j++) { doSomethingTimeConsumingWith(i, j);if(checkWatch() == OUT_OF_TIME) {goto giveUp; } } }giveUp: puts("done");The target for thegoto is alabel, which is just an identifier followed by a colon and a statement (the empty statement; is ok).
Thegoto statement can be used to jump anywhere within the same function body, but breaking out of nested loops is widely considered to be its only genuinely acceptable use in normal code.
Choosing where to put a loop exit is usually pretty obvious: you want it after any code that you want to execute at least once, and before any code that you want to execute only if the termination test fails.
If you know in advance what values you are going to be iterating over, you will most likely be using afor loop:
Most of the rest of the time, you will want awhile loop:
Thedo..while loop comes up mostly when you want to try something, then try again if it failed:
Finally, leaving a loop in the middle usingbreak can be handy if you have something extra to do before trying again:
for(;;) { result = fetchWebPage(url);if(result !=0) {break; }/* else */ fprintf(stderr,"fetchWebPage failed with error code %03d\n", result); sleep(retryDelay);/* wait before trying again */}(Note the emptyfor loop header means to loop forever;while(1) also works.)
Afunction,procedure, orsubroutine encapsulates some complex computation as a single operation. Typically, when wecall a function, we pass asarguments all the information this function needs, and any effect it has will be reflected in either itsreturn value or (in some cases) in changes to values pointed to by the arguments. Inside the function, the arguments are copied into local variables, which can be used just like any other local variable—they can even be assigned to without affecting the original argument.
A typical function definition looks like this:
/* Returns the square of the distance between two points separated by dx in the x direction and dy in the y direction. */intdistSquared(int dx,int dy){return dx*dx + dy*dy;}The part outside the braces is called thefunction declaration; the braces and their contents is thefunction body.
Like most complex declarations in C, once you delete the type names the declaration looks like how the function is used: the name of the function comes before the parentheses and the arguments inside. Theints scattered about specify the type of the return value of the function (before the function name) and of the parameters (inside the parentheses after the function name); these are used by the compiler to determine how to pass values in and out of the function and (usually for more complex types, since numerical types will often convert automatically) to detect type mismatches.
If you want to define a function that doesn’t return anything, declare its return type asvoid. You should also declare a parameter list ofvoid if the function takes no arguments.
It is not strictly speaking an error to omit the secondvoid here. Puttingvoid in for the parameters tells the compiler to enforce that no arguments are passed in. If we had instead declaredhelloWorld as
it would be possible to call it as
without causing an error. The reason is that a function declaration with no arguments means that the function can take an unspecified number of arguments, and it’s up to the user to make sure they pass in the right ones. There are good historical reasons for what may seem like obvious lack of sense in the design of the language here, and fixing this bug would break most C code written before 1989. But you shouldn’t ever write a function declaration with an empty argument list, since you want the compiler to know when something goes wrong.
As with any kind of abstraction, there are two goals to making a function:
Both of these goals may be trumped by the goal of making your code understandable. If you can’t describe what a function is doing in a single, simple sentence, this is a sign that maybe you need to restructure your code. Having a function that does more than one thing (or does different thing depending on its arguments) is likely to lead to confusion. So, for example, this is not a good function definition:
/***### UGLY CODE AHEAD### ***//* * If getMaximum is true, return maximum of x and y, * else return minimum. */intcomputeMaximumOrMinimum(int x,int y,int getMaximum){if(x > y) {if(getMaximum) {return x; }else {return y; } }else {if(getMaximum) {return y; }else {return x; } }}Better would be to write two functions:
/* return the maximum of x and y */intmaximum(int x,int y){if(x > y) {return x; }else {return y; }}/* return the minimum of x and y */intminimum(int x,int y){if(x < y) {return x; }else {return y; }}At the same time, it’s possible for a function to be too simple. Suppose I write the function
It’s pretty clear from the name what this function does. But since anybody who has been using C for a while has seenprintf("%d\n", ...) over and over again, it’s usually more clear to expand out the definition:
printIntWithNewline(2+5);/* this could do anything */ printf("%d\n",2+7);/* this does exactly what it says */As with all caveats, this caveat comes with its own caveat: what might justify a function like this is if you want to be able to do some kind of specialized formatting that should be consistent for all values of a particular form. So you might write aprintDistance function like the above as a stub for a fancier function that might use different units at different scales or something.
A similar issue will come up withnon-syntactic macros, which also tend to fail the “does this make my code more or less understandable” test. Usually it is a bad idea to try to replace common C idioms.
A function call consists of the function followed by its arguments (if any) inside parentheses, separated by comments. For a function with no arguments, call it with nothing between the parentheses. A function call that returns a value can be used in an expression just like a variable. A call to avoid function can only be used as an expression by itself:
To return a value from a function, write areturn statement, e.g.
The argument toreturn can be any expression. Unlike the expression in, say, anif statement, you do not need to wrap it in parentheses. If a function is declaredvoid, you can do areturn with no expression, or just let control reach the end of the function.
Executing areturn statement immediately terminates the function. This can be used likebreak to get out of loops early.
/* returns 1 if n is prime, 0 otherwise */intisPrime(int n){int i;if (n <2)return0;/* special case for 0, 1, negative n */for(i =2; i < n; i++) {if (n % i ==0) {/* found a factor */return0; } }/* no factors */return1;}By default, functions haveglobal scope: they can be used anywhere in your program, even in other files. If a file doesn’t contain a declaration for a functionsomeFunc before it is used, the compiler will assume that it is declared likeint someFunc() (i.e., return typeint and unknown arguments). This can produce infuriating complaints later when the compiler hits the real declaration and insists that your functionsomeFunc should be returning anint and you are a bonehead for declaring it otherwise.
The solution to such insulting compiler behavior errors is to either (a) move the function declaration before any functions that use it; or (b) put in a declaration without a body before any functions that use it, in addition to the declaration that appears in the function definition. (Note that this violates theno separate but equal rule, but the compiler should tell you when you make a mistake.) Option (b) is generally preferred, and is the only option when the function is used in a different file.
To make sure that all declarations of a function are consistent, the usual practice is to put them in an include file. For example, ifdistSquared is used in a lot of places, we might put it in its own filedistSquared.c:
The filedistSquared.c above uses#include to include a copy of the following header filedistSquared.h:
/* Returns the square of the distance between two points separated by dx in the x direction and dy in the y direction. */int distSquared(int dx,int dy);Note that the declaration indistSquared.h doesn’t have a body. Instead, it’s terminated by a semicolon, like a variable declaration. It’s also worth noting that we moved the documenting comment todistSquared.h: the idea is thatdistSquared.h is the public face of this (very small one-function) module, and so the explanation of how to use the function should be there.
The reasondistSquared.c includesdistSquared.h is to get the compiler to verify that the declarations in the two files match. But to use thedistSquared function, we also put#include "distSquared.h" at the top of the file that uses it:
#include"distSquared.h"#define THRESHOLD (100)inttooClose(int x1,int y1,int x2,int y2){return distSquared(x1 - x2, y1 - y2) < THRESHOLD;}The#include on line 1 uses double quotes instead of angle brackets; this tells the compiler to look fordistSquared.h in the current directory instead of the system include directory (typically/usr/include).
By default, all functions are global; they can be used in any file of your program whether or not a declaration appears in a header file. To restrict access to the current file, declare a functionstatic, like this:
staticvoidhelloHelper(void){ puts("hi!");}voidhello(int repetitions){int i;for(i =0; i < repetitions; i++) { helloHelper(); }}The functionhello will be visible everywhere. The functionhelloHelper will only be visible in the current file.
It’s generally good practice to declare a function static unless you intend to make it available, since not doing so can causenamespace conflicts, where the presence of two functions with the same name either prevent the program from linking or—even worse—cause the wrong function to be called. The latter can happen with library functions, since C allows the programmer to override library functions by defining a new function with the same name. Early on in my career as a C programmer, I once had a program fail in a spectacularly incomprehensible way because I’d written aselect function without realizing thatselect is a core library function in Unix.
A function may contain definitions oflocal variables, which are visible only inside the function and which survive only until the function returns. These may be declared at the start of any block (group of statements enclosed by braces), but it is conventional to declare all of them at the outermost block of the function.
/* Given n, compute n! = 1*2*...*n *//* Warning: will overflow on 32-bit machines if n > 12 */intfactorial(int n){int i;int product;if(n <2)return n;/* else */ product =1;for(i =2; i <= n; i++) { product *= i; }return product;}Several things happen under the hood when a function is called. Since a function can be called from several different places, the CPU needs to store its previous state to know where to go back. It also needs to allocate space for function arguments and local variables.
Some of this information will be stored inregisters, memory locations built into the CPU itself, but most will go on thestack, a region of memory that on typical machines grows downward, even though the most recent additions to the stack are called the “top” of the stack. The location of the top of the stack is stored in the CPU in a special register called thestack pointer.
So a typical function call looks like this internally:
return statement.From the programmer’s perspective, the important point is that both the arguments and the local variables inside a function are stored in freshly-allocated locations that are thrown away after the function exits. So after a function call the state of the CPU is restored to its previous state, except for the return value. Any arguments that are passed to a function are passed as copies, so changing the values of the function arguments inside the function has no effect on the caller. Any information stored in local variables is lost.
Under very rare circumstances, it may be useful to have a variable local to a function that persists from one function call to the next. You can do so by declaring the variablestatic. For example, here is a function that counts how many times it has been called:
/* return the number of times the function has been called */intcounter(void){static count =0;return ++count;}Static local variables are stored outside the stack with global variables, and have unbounded extent. But they are only visible inside the function that declares them. This makes them slightly less dangerous than global variables—there is no fear that some foolish bit of code elsewhere will quietly change their value—but it is still the case that they usually aren’t what you want. It is also likely that operations on static variables will be slightly slower than operations on ordinary (“automatic”) variables, since making them persistent means that they have to be stored in (slow) main memory instead of (fast) registers.
Pointers provided an abstraction for memory addresses. A pointer value specifies a location, but also has a type associated with it at compile time that allows the compiler to keep track of what is stored at that memory address. C uses pointers for a variety of tasks that are handled more abstractly in other programming languages, like returning extra values from functions or constructing arrays.
Memory in a typical modern computer is divided into two classes: a small number ofregisters, which live on the CPU chip and perform specialized functions like keeping track of the location of the next machine code instruction to execute or the current stack frame, andmain memory, which (mostly) lives outside the CPU chip and which stores the code and data of a running program. When the CPU wants to fetch a value from a particular location in main memory, it must supply an address: a 32-bit or 64-bit unsigned integer on typical current architectures, referring to one of up to232 or264 distinct 8-bit locations in the memory. These integers can be manipulated like any other integer; in C, they appear aspointers, a family of types that can be passed as arguments, stored in variables, returned from functions, etc.
Apointer variable is a variable that holds a pointer, just like anint variable is a variable that holds anint.
The convention is C is that the declaration of a complex type looks like its use. To declare a pointer-valued variable, write a declaration for the thing that it points to, but include a* before the variable name:
These declarations create four pointer variables, namedpointerToInt,pointerToDouble,pointerToChar, andpointerToPointerToChar. On a typical 64-bit machine, each will be allocated 8 bytes, enough to represent an address in memory.
The contents of these variables are initially arbitrary. To use them safely, you will need to compute the address of something and assign it to the variable.
Declaring a pointer-valued variable allocates space to hold the pointer butnot to hold anything it points to. Like any other variable in C, a pointer-valued variable will initially contain garbage—in this case, the address of a location that might or might not contain something important. To initialize a pointer variable, you have to assign to it the address of something that already exists. Typically this is done using the& (address-of) operator:
Pointer variables can be used in two ways. The simplest way is to get their value as with any other variable. This value will be an address, which can be stored in another pointer variable of the same type.
int n;/* an int variable */int *p;/* a pointer to an int */int *q;/* another pointer to an int */ p = &n;/* p now points to n */ q = p;/* q now points to n as well */But more often you will want to work on the value stored at the location pointed to. You can do this by using the* (dereference) operator, which acts as an inverse of the address-of operator:
int n;/* an int variable */int *p;/* a pointer to an int */ p = &n;/* p now points to n */ *p =2;/* sets n to 2 */ *p = *p + *p;/* sets n to 4 */The* operator binds very tightly, so you can usually use*p anywhere you could use the variable it points to without worrying about parentheses. However, a few operators, such as the-- and++ operators and the. operator used to unpackstructs, bind tighter. These require parentheses if you want the* to take precedence.
You can print a pointer value usingprintf with the%p format specifier. To do so, you should convert the pointer to thegeneric pointer typevoid * first using a cast, although on machines that don’t have different representations for different pointer types, this may not be necessary.
Here is a short program that prints out some pointer values:
#include<stdio.h>#include<stdlib.h>int G =0;/* a global variable, stored in BSS segment */intmain(int argc,char **argv){staticint s;/* static local variable, stored in BSS segment */int a;/* automatic variable, stored on stack */int *p;/* pointer variable for malloc below *//* obtain a block big enough for one int from the heap */ p = malloc(sizeof(int)); printf("&G = %p\n", (void *) &G); printf("&s = %p\n", (void *) &s); printf("&a = %p\n", (void *) &a); printf("&p = %p\n", (void *) &p); printf("p = %p\n", (void *) p); printf("main = %p\n", (void *) main); free(p);return0;}When I run this on a Mac OS X 10.6 machine after compiling withgcc, the output is:
&G = 0x100001078&s = 0x10000107c&a = 0x7fff5fbff2bc&p = 0x7fff5fbff2b0p = 0x100100080main = 0x100000e18The interesting thing here is that we can see how the compiler chooses to allocate space for variables based on their storage classes. The global variableG and the static local variables both persist between function calls, so they get placed in the BSS segment (see.bss) that starts somewhere around0x100000000, typically after the code segment containing the actual code of the program. Local variablesa andp are allocated on the stack, which grows down from somewhere near the top of the address space. The block returned frommalloc thatp points to is allocated off the heap, a region of memory that may also grow over time and starts after the BSS segment. Finally,main appears at 0x100000e18; this is in the code segment, which is a bit lower in memory than all the global variables.
The special value0, known as thenull pointer, may be assigned to a pointer of any type. It may or may not be represented by the actual address0, but it will act like0 in all contexts where an integer value is expected (e.g., it has the value false in anif orwhile statement). Null pointers are often used to indicate missing data or failed functions. Attempting to dereference a null pointer can have catastrophic effects, so it’s important to be aware of when you might be supplied with one.
A simple application of pointers is to get around C’s limit on having only one return value from a function. Because C arguments are copied, assigning a value to an argument inside a function has no effect on the outside. So thedoubler function below doesn’t do much:
#include<stdio.h>/* doesn't work */voiddoubler(int x){ x *=2;}intmain(int argc,char **argv){int y; y =1; doubler(y);/* no effect on y */ printf("%d\n", y);/* prints 1 */return0;}However, if instead of passing the value ofy intodoubler we pass a pointer toy, then thedoubler function can reach out of its own stack frame to manipulatey itself:
#include<stdio.h>voiddoubler(int *x){ *x *=2;}intmain(int argc,char **argv){int y; y =1; doubler(&y);/* sets y to 2 */ printf("%d\n", y);/* prints 2 */return0;}Generally, if you pass the value of a variable into a function (with no&), you can be assured that the function can’t modify your original variable. When you pass a pointer, you should assume that the function can and will change the variable’s value. If you want to write a function that takes a pointer argument but promises not to modify the target of the pointer, useconst, like this:
Theconst qualifier tells the compiler that the target of the pointer shouldn’t be modified. This will cause it to return an error if you try to assign to it anyway:
Passingconst pointers is mostly used when passing large structures to functions, where copying a 32-bit pointer is cheaper than copying the thing it points to.
If you really want to modify the target anyway, C lets you “cast awayconst”:
voidprintPointerTarget(constint *p){ *((int *) p) =5;/* no compile-time error */ printf("%d\n", *p);}There is usually no good reason to do this. The one exception might be if the target of the pointer represents anabstract data type, and you want to modify its representation during some operation to optimize things somehow in a way that will not be visible outside the abstraction barrier, making it appear to leave the target constant.
Note that while it is safe to pass pointers down into functions, it is very dangerous to pass pointers up. The reason is that the space used to hold any local variable of the function will be reclaimed when the function exits, but the pointer will still point to the same location,even though something else may now be stored there. So this function is very dangerous:
int *dangerous(void){int n;return &n;/* NO! */}... *dangerous() =12;/* writes 12 to some unknown location */An exception is when you can guarantee that the location pointed to will survive even after the function exits, e.g. when the location is dynamically allocated usingmalloc (see below) or when the local variable is declaredstatic:
int *returnStatic(void){staticint n;return &n;}... *returnStatic() =12;/* writes 12 to the hidden static variable */Usually returning a pointer to astatic local variable is not good practice, since the point of making a variable local is to keep outsiders from getting at it. If you find yourself tempted to do this, a better approach is to allocate a new block usingmalloc (see below) and return a pointer to that. The downside of themalloc method is that the caller has to promise to callfree on the block later, or you will get a storage leak.
Because pointers are just numerical values, one can do arithmetic on them. Specifically, it is permitted to
p+n wherep is a pointer andn is an integer is to compute the address equal top plusn times the size of whateverp points to (this is whyint * pointers andchar * pointers aren’t the same).int * or bothchar *). The result is a signed integer value of typeptrdiff_t, equal to the numerical difference between the addresses divided by the size of the objects pointed to.==,!=,<,>,<=, or>=.++ or--.The main application of pointer arithmetic in C is inarrays. An array is a block of memory that holds one or more objects of a given type. It is declared by giving the type of object the array holds followed by the array name and the size in square brackets:
Declaring an array allocates enough space to hold the specified number of objects (e.g. 200 bytes fora above and 400 forcp—note that achar * is an address, so it is much bigger than achar). The number inside the square brackets must be a constant whose value can be determined at compile time.
The array name acts like a constant pointer to the zeroth element of the array. It is thus possible to set or read the zeroth element using*a. But because the array name is constant, you can’t assign to it:
More common is to use square brackets to refer to a particular element of the array. The expressiona[n] is defined to be equivalent to*(a+n); theindexn (an integer) is added to the base of the array (a pointer), to get to the location of then-th element ofa. The implicit* then dereferences this location so that you can read its value (in a normal expression) or assign to it (on the left-hand side of an assignment operator). The effect is to allow you to usea[n] just as you would any other variable of typeint (or whatever typea was declared as).
Note that C doesn’t do any sort of bounds checking. Given the declarationint a[50];, only indices froma[0] toa[49] can be used safely. However, the compiler will not blink ata[-12] ora[10000]. If you read from such a location you will get garbage data; if you write to it, you will overwrite god-knows-what, possibly trashing some other variable somewhere else in your program or some critical part of the stack (like the location to jump to when you return from a function). It is up to you as a programmer to avoid suchbuffer overruns, which can lead to very mysterious (and in the case of code that gets input from a network, security-damaging) bugs. Thevalgrind program can help detect such overruns in some cases.
Another curious feature of the definition ofa[n] as identical to*(a+n) is that it doesn’t actually matter which of the array name or the index goes inside the braces. So all ofa[0],*a, and0[a] refer to the zeroth entry ina. Unless you are deliberately trying to obfuscate your code, it’s best to write what you mean.
Because array names act like pointers, they can be passed into functions that expect pointers as their arguments. For example, here is a function that computes the sum of all the values in an arraya of sizen:
/* compute the sum of the first n elements of array a */intsumArray(int n,constint *a){int i;int sum; sum =0;for(i =0; i < n; i++) { sum += a[i]; }return sum;}Note the use ofconst to promise thatsumArray won’t modify the contents ofa.
Another way to write the function header is to declarea as an array of unknown size:
This hasexactly the same meaning to the compiler as the previous definition. Even though normally the declarationsint a[10] andint *a mean very different things (the first one allocates space to hold 10ints, and prevents assigning a new value toa), in a function argumentint a[] is justsyntactic sugar forint *a. You can even modify whata points to insidesumArray by assigning to it. This will allow you to do things that you usually don’t want to do, like write this hideous routine:
/* return the sum of the first n values in a */intsumArray(int n,constint a[]){constint *an;/* pointer to first element not in a */int sum; sum =0; an = a+n;while(a < an) { sum += *a++; }return sum;}Arrays can themselves be members of arrays. The result is a multidimensional array, where a value in rowi and columnj is accessed bya[i][j].
There are actually two different ways to do this that both support thea[i][j] syntax. Withstandard C multidimensional arrays, the array is an array of one-dimensional arrays, each of which has the same fixed size. With anarray of pointers, the array is an array of pointers, each of which points to the first element of a one-dimensional array. The standard approach is simpler to set up and avoids some extra time and space complexity, but it breaks down for very large arrays or when the rows of the array may have different lengths. We’ll describe how to do both, as well as a third option thatrepresents a two-dimensional array directly as a one-dimensional array, at the cost of losing the standarda[i][j] lookup syntax.
Declaration is similar to one-dimensional arrays:
This declaration produces an array of 18int values, packed contiguously in memory. The interpretation is thata is an array of 3 objects, each of which is an array of 6ints.
If we imagine the array to contain increasing values like this:
0 1 2 3 4 5 6 7 8 9 10 1112 13 14 15 16 17the actual positions in memory will look like this:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ^ ^ ^a[0] a[1] a[2]To look up a value, we do the usual array-indexing magic. Suppose we want to finda[1][4]. The namea acts as a pointer to the base of the array.The namea[1] says to skip ahead 1 times the size of the things pointed to bya, which are arrays of 6ints each, for a total size of 24 bytes assuming 4-byteints. Fora[1][4], we start ata[1] and move forward 4 times the size of the thing pointed to bya[1], which is anint; this puts us 24+16 bytes froma, the position of 10 in the picture above.
It’s useful to understand what is happening in this example, and for small arrays of fixed size there is no particular reason not to use C’s built-in arrays, but as soon as you start dealing with large arrays or arrays whose size is not fixed, you are better off doing something else. (This is true even if you use C99-stylevariable-length arrays.) Two possible approaches that you could try areusing an malloc’d array of pointers to malloc’d rows, which has the advantage of preserving thea[i][j] syntax; andpacking a two-dimensional array into a malloc’d one-dimensonal array, which has the advantage of preserving contiguity. Both approaches are described below.
Here we allocate each row separately usingmalloc and building a master list of pointers to rows, of typeint **. The downside of this approach is that the array is no longer contiguous (which may affect cache performance) and it requires reading a pointer to find the location of a particular value, instead of just doing address arithmetic starting from the base address of the array. But elements can still be accessed using thea[i][j] syntax.
The naive way to do this is to allocate each row with a separate call tomalloc. But since we know the total size of all of the rows, we can save both time and space by allocating one giant block and partitioning this block into rows ourselves. This may also help with cache performance since the array contents are all stored in one place, although we still have to follow a row pointer.
Note this can work even if the rows have different lengths (for example, if each is a null-terminated string), as long as we are careful to add up the lengths correctly and set the pointers to the right places.
Two examples of this approach are given below. The first builds a two-dimensional array ofints for any given number of rows and columns, while the second copies a collection of null-terminated strings into a singlemalloc’d block.
/* Demo program for malloc'd two-dimensional arrays */#include<stdio.h>#include<stdlib.h>/* frees a 2d array created by malloc2d */voidfree2d(void **a){/* free the rows */ free(a[0]);/* then free array of pointers */ free(a);}/* returns a two-dimensional array with numRows rows and * rowSize bytes per row, or 0 on allocation failure. * The caller is responsible for freeing the result with free2d. */void **malloc2d(size_t numRows,size_t rowSize){void **a;size_t i;/* a is an array of void * pointers that point to the rows */ a = malloc(sizeof(void *) * numRows);if(a ==0) {/* malloc failed */return0; }/* now allocate the actual rows *//* the trick here is that we only allocate one big block */ a[0] = malloc(rowSize * numRows);if(a[0] ==0) { free(a);return0; }// fill in remaining row pointersfor(i =1; i < numRows; i++) {// compute offset to start of row i by hand a[i] = a[0] + rowSize * i; }return a;}intmain(int argc,char **argv){int rows;int cols;int **a;int i;int j;if(argc !=3) { fprintf(stderr,"Usage: %s rows cols\n", argv[0]);return1; }/* else */ rows = atoi(argv[1]); cols = atoi(argv[2]);/* note that void ** is not converted automatically, * so we need an explicit cast */ a = (int **) malloc2d(rows, cols *sizeof(int));if(a ==0) { fprintf(stderr,"malloc2d failed, exiting\n");return2; }for(i =0; i < rows; i++) {for(j =0; j < cols; j++) { a[i][j] = i - j; } }for(i =0; i < rows; i++) {for(j =0; j < cols; j++) { printf("%4d", a[i][j]); } putchar('\n'); } free2d((void **) a);/* always clean up */return0;}#include<stdio.h>#include<stdlib.h>#include<string.h>// given n pointers to strings, pack copies of them// into a single malloc'd block that can be accessed// using bracket syntaxchar **packStrings(size_t n,char *s[]){size_t length =0;// compute total length including nullsfor(size_t i =0; i < n; i++) { length += strlen(s[i]) +1; }// allocate a block big enough for pointers and stringschar **s2 = malloc(sizeof(char *) * n + length);// this is a pointer to where the strings are copied// after the n pointerschar *top = (char *) (s2 + n);// copy the strings and fill in the pointer arrayfor(size_t i =0; i < n; i++) { strcpy(top, s[i]); s2[i] = top; top += strlen(s[i]) +1; }return s2;}voidfreePackedStrings(char **s){ free(s);}intmain(int argc,char **argv){// pack argv, then print it out from the copied versionchar **argv2 = packStrings(argc, argv);for(int i =0; i < argc; i++) { puts(argv2[i]); } freePackedStrings(argv2);return0;}A third approach is to store a two-dimensional array in a one-dimensional array, and do the indexing arithmetic ourselves. Since this requires keeping track of the dimensions of the array as well as the contents, it helps to be able to wrap up these values in astruct (seeStructs), and possibly even hide the details of the construction behind anabstract data type. Typically we will provide operations to create an array with given dimensions, to destroy an array (freeing any space we malloc’d), and to obtain a pointer to a particular location in an array, which we can then use to operate on that location.
The advantages of this approach are that the contents of the array are packed contiguously and that address lookup doesn’t require following multiple pointers, which becomes particularly helpful if we generalize to more than two dimensions. We can also enforce bounds checking for safety (although at the cost of some time). The main disadvantage is that we lose access to thea[i][j] syntax.
An example of a simple implementation of a two-dimensional array is given below.
/* Demo program for packed two-dimensional arrays */#include<stdio.h>#include<stdlib.h>#include<assert.h>struct flat2d {size_t rows;// number of rowssize_t cols;// number of columnsint data[];// data, stored at end of struct};// malloc and return flat2d of given dimensions// array is not initialized!struct flat2d *flat2dCreate(size_t rows,size_t cols){struct flat2d *a; a = malloc(sizeof(struct flat2d) +sizeof(int) * rows * cols); assert(a); a->rows = rows; a->cols = cols;return a;}// free space used by avoidflat2dDestroy(struct flat2d *a){ free(a);}// return a pointer to a[i][j]// or 0 if i or j is out of boundsint *flat2dRef(struct flat2d *a,size_t i,size_t j){if(i >= a->rows || j >= a->cols) {return0; }else {return &a->data[i * a->cols + j]; }}intmain(int argc,char **argv){int rows;int cols;struct flat2d *a;int i;int j;if(argc !=3) { fprintf(stderr,"Usage: %s rows cols\n", argv[0]);return1; }/* else */ rows = atoi(argv[1]); cols = atoi(argv[2]); a = flat2dCreate(rows, cols);for(i =0; i < rows; i++) {for(j =0; j < cols; j++) { *flat2dRef(a, i, j) = i - j; } }for(i =0; i < rows; i++) {for(j =0; j < cols; j++) { printf("%4d", *flat2dRef(a, i, j)); } putchar('\n'); } flat2dDestroy(a);return0;}C99 adds the feature ofvariable-length arrays, where the size of the array is determined at run-time. These can only appear as local variables in procedures (automatic variables) or in argument lists. In the case of variable-length arrays in argument lists, it is also necessary that the length of the array be computable from previous arguments.
For example, we could make the length of the array explicit in oursumArray function:
/* return the sum of the values in a, an array of size n */intsumArray(int n,constint a[n]){int i;int sum; sum =0;for(i =0; i < n; i++) { sum += a[i]; }return sum;}This doesn’t accomplish much, because the length of the array is not used. However, it does become useful if we have a two-dimensional array, as otherwise there is no way to compute the length of each row:
intsumMatrix(int rows,int cols,constint m[rows][cols]){int i;int j;int sum; sum =0;for(i =0; i < rows; i++) {for(j =0; j < cols; j++) { sum += a[i][j]; } }return sum;}Here the fact that each row ofm is known to be an array ofcols manyints makes the implicit pointer computation ina[i][j] actually work. It is considerably more difficult to to this in ANSI C; the simplest approach is to packm into a one-dimensional array and do the address computation explicitly:
intsumMatrix(int rows,int cols,constint a[]){int i;int j;int sum; sum =0;for(i =0; i < rows; i++) {for(j =0; j < cols; j++) { sum += a[i*cols + j]; } }return sum;}Variable-length arrays can sometimes be used for run-time storage allocation, as an alternative tomalloc andfree (see below). A variable-length array allocated as a local variable will be deallocated when the containing scope (usually a function body, but maybe just a compound statement marked off by braces) exits. One consequence of this is that you can’t return a variable-length array from a function.
Here is an example of code using this feature:
/* reverse an array in place */voidreverseArray(int n,int a[n]){/* algorithm: copy to a new array in reverse order *//* then copy back */int i;int copy[n];for(i =0; i < n; i++) {/* the -1 is needed to that a[0] goes to a[n-1] etc. */ copy[n-i-1] = a[i]; }for(i =0; i < n; i++) { a[i] = copy[i]; }}While using variable-length arrays can simplify code in some cases, as a general programming practice it isextremely dangerous. The reason is that, unlike allocations throughmalloc, variable-length array allocations are typically allocated on the stack (which is often more constrainted than the heap) and have no way of reporting failure. So if there isn’t enough room for your variable-length array, odds are you won’t find out until a segmentation fault occurs somewhere later in your code when you try to use it.
(As an additional annoyance,gdb is confused by two-dimensional variable-length arrays.)
Here’s a safer version of the above routine, usingmalloc andfree.
/* reverse an array in place */voidreverseArray(int n,int a[n]){/* algorithm: copy to a new array in reverse order *//* then copy back */int i;int *copy; copy = (int *) malloc(n *sizeof(int)); assert(copy);/* or some other error check */for(i =0; i < n; i++) {/* the -1 is needed to that a[0] goes to a[n-1] etc. */ copy[n-i-1] = a[i]; }for(i =0; i < n; i++) { a[i] = copy[i]; } free(copy);}A special pointer type isvoid *, a “pointer tovoid”. Such pointers are declared in the usual way:
Unlike ordinary pointers, you can’t dereference avoid * pointer or do arithmetic on it, because the compiler doesn’t know what type it points to. However, you are allowed to use avoid * as a kind of “raw address” pointer value that you can store arbitrary pointers in. It is permitted to assign to avoid * variable from an expression of any pointer type; conversely, avoid * pointer value can be assigned to a pointer variable of any type. An example is the return value ofmalloc or the argument tofree, both of which are declared asvoid *. (Note that K&R suggests using an explicit cast for the return value ofmalloc. This has since been acknowledged by the authors to be an error, which arose from the need for a cast prior to the standardization ofvoid * in ANSI C.)
int *block; block = malloc(sizeof(int) *12);/* void * converted to int * before assignment */ free(block);/* int * converted to void * before passing to free */If you need to use avoid * pointer as a pointer of a particular type in an expression, you cancast it to the appropriate type by prefixing it with a type name in parentheses, like this:
int a[50];/* typical array of ints */void *p;/* dangerous void pointer */ a[12] =17;/* save that valuable 17 */ p = a;/* p now holds base address of a */ printf("%d\n", ((int *) p)[12]);/* get 17 back */Usually if you have to start writing casts, it’s a sign that you are doing something wrong, and you run the danger ofviolating the type system—say, by tricking the compiler into treating a block of bits that are supposed to be anint as fourchars. But violating the type system like this will be necessary for some applications, because even the weak type system in C turns out to be too restrictive for writing certain kinds of “generic” code that work on values of arbitrary types.
One issue with casting pointers to and fromvoid * is that you may violate thealignment restrictions for a particular kind of pointer on some architectures.
Back in the 8-bit era of the 1970s, a single load or store operation would access a single byte of memory, and because some data (chars) are still only one byte wide, C pointers retain the ability to address individual bytes. But present-day memory architectures typically have a wider data path, and the CPU may load or store as many as 8 bytes (64 bits) in a single operation. This makes it natural to organize memory into 4-byte or 8-byte words even though addresses still refer to individual bytes. The effect of the memory architecture is that the address of memory words must bealigned to a multiple of the word size: so with 4-byte words, the address0x1037ef44 (a multiple of 4) could refer to a full word, but0x1037ef45 (one more than a multiple of 4) could only be used to refer to a byte within a word.
What this means for a C program depends on your particular CPU and compiler. If you try to use something like0x1037ef45 as anint *, one of three things might happen:
int out of fragments of words. This is done on Intel architectures, but costs performance.0x1037ef44 even though you asked for0x1037ef45. This happens on some other architectures, notably ARM.All of these outcomes are bad, and the C standard does not specify what happens if you try to dereference a pointer value that does not satisfy the alignment restrictions of its target type. Fortunately, unless you are doing very nasty things with casts, this is unlikely to come up, because any pointer value you will see in a typical program is likely to arise in one of three ways:
structs) with appropriate alignment.p + n) or implicitly (p[n]). In either case, as long as the base pointer is correctly aligned, the computed pointer will also be correctly aligned.malloc or a similar function. Heremalloc is designed to always return blocks with the maximum possible required alignment, just to avoid problems when you use the results elsewhere.On many compilers, you can use__alignof(type) to get the alignment restriction for a particular type. This was formalized in C11 without the underscores:alignof. Usually if your code needs to include__alignof oralignof something has already gone wrong.
The other place where alignment can create issues is that if you make astruct with components with different alignment restrictions, you may end up with some empty space. For example, on a machine that enforces 4-byte alignment forints, building astruct that contains achar and anint will give you something bigger than you might expect:
#include<stdio.h>struct ci {char c;/* offset 0 *//* 3 unused bytes go here */int i;/* offset 4 */};struct ic {int i;/* offset 0 */char c;/* offset 4 *//* 3 unused bytes go here */};intmain(int argc,char **argv){ printf("sizeof(struct ci) == %lu\n",sizeof(struct ci)); printf("sizeof(struct ic) == %lu\n",sizeof(struct ic));return0;}$ gcc -Wall -o structPacking structPacking.c$ ./structPackingsizeof(struct ci) == 8sizeof(struct ic) == 8In both cases, the compiler packs in an extra 3 bytes to make the size of the struct a multiple of the worst alignment of any of its components. If it didn’t do this, you would have trouble as soon as you tried to make an array of these things.
mallocC does not generally permit arrays to be declared with variable sizes. C also doesn’t let local variables outlive the function they are declared in. Both features can be awkward if you want to build data structures at run time that have unpredictable (perhaps even changing) sizes and that are intended to persist longer than the functions that create them. To build such structures, the standard C library provides themalloc routine, which returns a otherwise-unused block of space of a given size (in bytes).
Unlike local variables that expire when their function returns, you can use this space for whatever you want for as long as you want. When you are done with it, you should call thefree routine to give it back. Themalloc andfree routines together act as brokers for space on the heap, whichmalloc will grow as needed by asking for more space from the operating system.
To usemalloc, you must includestdlib.h at the top of your program. The declaration formalloc is
wheresize_t is an integer type (oftenunsigned long). Callingmalloc with an argument ofn allocates and returns a pointer to the start of a block ofn bytes if possible. If the system can’t give you the space you asked for (maybe you asked for more space than it has),malloc returns a null pointer. It is good practice to test the return value ofmalloc whenever you call it.
Because the return type ofmalloc isvoid *, its return value can be assigned to any variable with a pointer type. Computing the size of the block you need is your responsibility—and you will be punished for any mistakes with difficult-to-diagnose buffer overrun errors—but this task is made slightly easier by the built-insizeof operator that allows you to compute the size in bytes of any particular data type. A typical call tomalloc might thus look something like this:
#include<stdlib.h>/* allocate and return a new integer array with n elements *//* calls abort() if there isn't enough space */int *makeIntArray(int n){int *a; a = malloc(sizeof(int) * n);if(a ==0) abort();/* die on failure */return a;}If you don’t want to do the multiplication yourself, or if you want to guarantee that the allocated data is initialized to zero, you can usecalloc instead ofmalloc. Thecalloc function is also declared instdlib.h and takes two arguments: the number of things to allocate, and the size of each thing. Here’s a version ofmakeIntArray that usescalloc.
#include<stdlib.h>/* allocate and return a new integer array with n elements *//* initializes array to zero *//* calls abort() if there isn't enough space */int *makeIntArray(int n){int *a; a = calloc(n,sizeof(int));if(a ==0) abort();/* die on failure */return a;}If you know that you want to initialize your data to zero (which means all zero bytes, which will translate to a zero or null value for typical C data types),calloc can be significantly more efficient thanmalloc. The reason is that zero-value memory pages can often be requested from the operating system without actually allocating space for them until they are written, so allocating huge blocks usingcalloc doesn’t take much more time than allocating small ones. If we usedmalloc and then initialized the blocks by hand, we would have to pay both the time cost of filling in all the initial values and the space cost of allocating pages that we might not actually need. Socalloc is often a better choice in these cases. However, if you are planning on filling in all the data eventually,calloc just shifts the cost to the time when you write to the pages, so the difference mostly just affects whether you pay a big cost up front or spread it out over the execution of your program.
Whichever ofmalloc orcalloc you use, when you are done with a block, you should return its space to the storage allocator using thefree routine, also defined instdlib.h. If you don’t do this, your program may quickly run out of space. Thefree routine takes avoid * as its argument and returns nothing. It is good practice to write a matchingdestructor that de-allocates an object for eachconstructor (likemakeIntArray) that makes one.
It is a serious error to do anything at all with a block after it has beenfreed. This is not necessarily becausefree modifies the contents of the block (although it might), but because when you free a block you are granting the storage allocator permission to hand the same block out in response to a future call tomalloc, and you don’t want to step on whatever other part of your program is now trying to use that space.
It is also possible to grow or shrink a previously allocated block. This is done using therealloc function, which is declared as
Therealloc function returns a pointer to the resized block. It may or may not allocate a new block. If there is room, it may leave the old block in place and return its argument. But it may allocate a new block and copy the contents of the old block, so you should assume that the old pointer has beenfreed.
Here’s a typical use ofrealloc to build an array that grows as large as it needs to be:
/* read numbers from stdin until there aren't any more *//* returns an array of all numbers read, or null on error *//* returns the count of numbers read in *count */int *readNumbers(int *count/* RETVAL */){int mycount;/* number of numbers read */int size;/* size of block allocated so far */int *a;/* block */int n;/* number read */ mycount =0; size =1; a = malloc(sizeof(int) * size);/* allocating zero bytes is tricky */if(a ==0)return0;while(scanf("%d", &n) ==1) {/* is there room? */while(mycount >= size) {/* double the size to avoid calling realloc for every number read */ size *=2; a = realloc(a,sizeof(int) * size);if(a ==0)return0; }/* put the new number in */ a[mycount++] = n; }/* now trim off any excess space */ a = realloc(a,sizeof(int) * mycount);/* note: if a == 0 at this point we'll just return it anyway *//* save out mycount */ *count = mycount;return a;}Because errors involvingmalloc and its friends can be very difficult to spot, it is recommended to test any program that usesmalloc usingvalgrind.
Afunction pointer, internally, is just the numerical address for the code for a function. When a function name is used by itself without parentheses, the value is a pointer to the function, just as the name of an array by itself is a pointer to its zeroth element. Function pointers can be stored in variables,structs,unions, and arrays and passed to and from functions just like any other pointer type. They can also be called: a variable of type function pointer can be used in place of a function name.
Function pointers are not used as much in C as in functional languages, but there are many common uses even in C code.
A function pointer declaration looks like a function declaration, except that the function name is wrapped in parentheses and preceded by an asterisk. For example:
/* a function taking two int arguments and returning an int */int function(int x,int y);/* a pointer to such a function */int (*pointer)(int x,int y);As with function declarations, the names of the arguments can be omitted.
Here’s a short program that uses function pointers:
/* Functional "hello world" program */#include<stdio.h>intmain(int argc,char **argv){/* function for emitting text */int (*say)(constchar *); say = puts; say("hello world");return0;}Acallback is when we pass a function pointer into a function so that that function can call our function when some event happens or it needs to compute something.
A classic example is the comparison argument toqsort, from the standard library:
/* defined in stdlib.h */voidqsort(void *base,size_t n,size_t size,int (*cmp)(constvoid *key1,constvoid *key2));This is a generic sorting routine that will sort any array in place. It needs to know (a) the base address of the array; (b) how many elements there are; (c) how big each element is; and (d) how to compare two elements. The only tricky part is supplying the comparison, which could involve arbitrarily-complex code. So we supply this code as a function with an interface similar tostrcmp.
staticintcompare_ints(void *key1,void *key2){return *((int *) key1) - *((int *) key2);}intsort_int_array(int *a,int n){ qsort(a, n,sizeof(*a), compare_ints);}Other examples might include things like registering an error handler for a library, instead of just having it callabort() or something equally catastrophic, or providing a cleanup function for freeing data passed into a data structure.
Dispatch tables are an alternative to giganticif/else if orswitch statements. The idea is to build an array of function pointers (or, more generally, some sort ofdictionary data structure), and use the value we might otherwise be feeding toswitch as an index into this array. Here is a simple example, which echoes most of the characters in its input intact, except for echoing every lowercase vowel twice:
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<limits.h>/* * Demonstrate use of dispatch tables. *//* print a character twice *//* like putchar, returns character if successful or EOF on error */intputcharTwice(int c){if(putchar(c) == EOF || putchar(c) == EOF) {return EOF; }else {return c; }}#define NUM_CHARS (UCHAR_MAX + 1)/* UCHAR_MAX is in limits.h */intmain(int argc,char **argv){/* this declares table as an array of function pointers */int (*table[UCHAR_MAX+1])(int);int i;int c;for(i =0; i < UCHAR_MAX; i++) {/* default is to call putchar */ table[i] = putchar; }/* but lower-case vowels show up twice */ table['a'] = putcharTwice; table['e'] = putcharTwice; table['i'] = putcharTwice; table['o'] = putcharTwice; table['u'] = putcharTwice;while((c = getchar()) != EOF) { table[c](c); }return0;}And here is the program translating Shakespeare into mock-Swedish:
$ gcc -Wall -g3 -o dispatchTable dispatchTable.c $ echo Now is the winter of our discontent made glorious summer by this sun of York. | ./dispatchTable Noow iis thee wiinteer oof oouur diiscoonteent maadee glooriioouus suummeer by thiis suun oof Yoork.In this particular case, we did a lot of work to avoid just writing aswitch statement. But being able to build a dispatch table dynamically can be very useful sometimes. An example might be a graphical user interface where each button has an associated function. If buttons can be added by different parts of the program, using a table mapping buttons to functions allows a single dispatch routine to figure out where to route button presses.
(For some applications, we might want to pass additional information in to the function to change its behavior. This can be done by replacing the function pointers withclosures.)
In C99, it is possible to declare that a pointer variable is the only way to reach its target as long as it is in scope. This is not enforced by the compiler; instead, it is a promise from the programmerto the compiler that any data reached through this point will not be changed by other parts of the code, which allows the compiler to optimize code in ways that are not possible if pointers might point to the same place (a phenomenon calledpointer aliasing). For example, consider the following short function:
// write 1 + *src to *dst and return *srcintcopyPlusOne(int *restrict dst,int *restrict src){ *dst = *src +1;return *src;}For this function, the output ofgcc -O3 -S includes one more instruction if therestrict qualifiers are removed. The reason is that ifdst andsrc may point to the same location,src needs to be re-read for thereturn statement, in case it changed. But if they are guaranteed to point to different locations, the compiler can re-use the previous value it already has in one of the CPU registers.
For most code, this feature is useless, and potentially dangerous if someone calls your routine with aliased pointers. However, it may sometimes be possible to increase performance of time-critical code by adding arestrict keyword. The cost is that the code might no longer work if called with aliased pointers.
Curiously, C assumes that two pointers are never aliased if you have two arguments with different pointer types, neither of which ischar * orvoid *.10 This is known as thestrict aliasing rule and cannot be overridden from within the program source code: there is nounrestrict keyword. You probably only need to worry about this if you are casting pointers to different types and then passing the cast pointers around in the same context as the original pointers.
Processing strings of characters is one of the oldest application of mechanical computers, arguably predating numerical computation by at least fifty years. Assuming you’ve already solved the problem of how to represent characters in memory (e.g. as the Cchar type encoded inASCII), there are two standard ways to represent strings:
Because delimited strings are simpler and take less space, C went for delimited strings. A string is a sequence of characters terminated by a null character'\0'. Looking back from almost half a century later, this choicemay have been a mistake in the long run, but we are pretty much stuck with it.
Note that the null character isnot the same as a null pointer, although both appear to have the value0 when used in integer contexts. A string is represented by a variable of typechar *, which points to the zeroth character of the string. The programmer is responsible for allocating and managing space to store strings, except for explicitstring constants, which are stored in a special non-writable string space by the compiler.
If you want to use counted strings instead, you can build your own using astruct. Most scripting languages written in C (e.g.Perl,Python_programming_language,PHP, etc.) use this approach internally. (Tcl is an exception, which is one of many good reasons not to use Tcl).
A string constant in C is represented by a sequence of characters within double quotes. Standard C character escape sequences like\n (newline),\r (carriage return),\a (bell),\0x17 (character with hexadecimal code0x17),\\ (backslash), and\" (double quote) can all be used inside string constants. The value of a string constant has typeconst char *, and can be assigned to variables and passed as function arguments or return values of this type.
Two string constants separated only by whitespace will be concatenated by the compiler as a single constant:"foo" "bar" is the same as"foobar". This feature is not much used in normal code, but shows up sometimes inmacros.
Standard C strings are assumed to be inASCII, a 7-bit code developed in the 1960s to represent English-language text. If you want to write text that includes any letters not in the usual 26-letter Latin alphabet, you will need to use a different encoding. C does not provide very good support for this, but for fixed strings, you can often get away with usingUnicode as long as both your text editor and your terminal are set to use theUTF-8 encoding.
The reason this works is that UTF-8 encodes each Unicode character as one or more 8-bit characters, and does this in a way that guarantees that you never accidentally create a null or any other standard ASCII character. So a C string containing UTF-8 characters looks like an ordinary C string to all the C library routines. This also works if you include a Unicode string with a UTF-8 encoding in a comment orprintf format string, as illustrated in the fileunicode.c. But this use of Unicode in C is very limited.
Some issues you will quickly run into if you are trying to do something more sophisticated:
char variable, or write it as achar constant.There exists libraries for working with Unicode strings in C, but they are clunky. If you need to handle a lot of non-ASCII text, you may be better of working with a different language. However, even moving away from C is not always a panacea, and Unicode support in other tools may be hit-or-miss.
The problem with string constants is that you can’t modify them. If you want to build strings on the fly, you will need to allocate space for them. The traditional approach is to use abuffer, an array ofchars. Here is a particularly painful hello-world program that builds a string by hand:
#include<stdio.h>intmain(int argc,char **argv){char hi[3]; hi[0] ='h'; hi[1] ='i'; hi[2] = '\0'; puts(hi);return0;}Note that the buffer needs to have size at least 3 in order to hold all three characters. A common error in programming with C strings is to forget to leave space for the null at the end (or to forget to add the null, which can have comical results depending on what you are using your surprisingly long string for).
getsFixed-size buffers are a common source of errors in older C programs, particularly ones written with the library routinegets. The problem is that if you do something like
thestrcpy function will happily keep copying characters across memory long after it has passed the end ofsmallBuffer. While you can avoid this to a certain extent when you control wherebigString is coming from, the situation becomes particularly fraught if the string you are trying to store comes from the input, where it might be supplied by anybody, including somebody who is trying to execute abuffer overrun attack to seize control of your program.
If you do need to read a string from the input, you should allocate the receiving buffer usingmalloc and expand it usingrealloc as needed. Below is a program that shows how to do this, with some bad alternatives commented out:
#include<stdio.h>#include<stdlib.h>#include<assert.h>#define NAME_LENGTH (2)#define INITIAL_LINE_LENGTH (2)/* return a freshly-malloc'd line with next line of input from stdin */char *getLine(void){char *line;int size;/* how much space do I have in line? */int length;/* how many characters have I used */int c; size = INITIAL_LINE_LENGTH; line = malloc(size); assert(line); length =0;while((c = getchar()) != EOF && c !='\n') {if(length >= size-1) {/* need more space! */ size *=2;/* make length equal to new size *//* copy contents if necessary */ line = realloc(line, size); } line[length++] = c; } line[length] = '\0';return line;}intmain(int argc,char **argv){int x =12;/* char name[NAME_LENGTH]; */char *line;int y =17; puts("What is your name?");/* gets(name); *//* may overrun buffer *//* scanf("%s\n", name); *//* may overrun buffer *//* fgets(name, NAME_LENGTH, stdin); *//* may truncate input */ line = getLine();/* has none of these problems */ printf("Hi %s! Did you know that x == %d and y == %d?\n", line, x, y); free(line);/* but we do have to free line when we are done with it */return0;}Unlike many programming languages, C provides only a rudimentary string-processing library. The reason is that many common string-processing tasks in C can be done very quickly by hand.
For example, suppose we want to copy a string from one buffer to another. The library functionstrcpy declared instring.h will do this for us (and is usually the right thing to use), but if it didn’t exist we could write something very close to it using a famous C idiom.
voidstrcpy2(char *dest,constchar *src){/* This line copies characters one at a time from *src to *dest. *//* The postincrements increment the pointers (++ binds tighter than *) *//* to get to the next locations on the next iteration through the loop. *//* The loop terminates when *src == '\0' == 0. *//* There is no loop body because there is nothing to do there. */while(*dest++ = *src++);}The externally visible difference betweenstrcpy2 and the originalstrcpy is thatstrcpy returns achar * equal to its first argument. It is also likely that any implementation ofstrcpy found in a recent C library takes advantage of the width of the memory data path to copy more than one character at a time.
Most C programmers will recognize thewhile(*dest++ = *src++); from having seen it before, although experienced C programmers will generally be able to figure out what such highly abbreviated constructions mean. Exposure to such constructions is arguably a form of hazing.
Because C pointers act exactly like array names, you can also writestrcpy2 using explicit array indices. The result is longer but may be more readable if you aren’t a C fanatic.
char *strcpy2a(char *dest,constchar *src){int ; i =0;for(i =0; src[i] != '\0'; i++) { dest[i] = src[i]; }/* note that the final null in src is not copied by the loop */ dest[i] = '\0';return dest;}An advantage of using a separate index instrcpy2a is that we don’t trashdest, so we can return it just likestrcpy does. (In fairness,strcpy2 could have saved a copy of the original location ofdest and done the same thing.)
Note that nothing instrcpy2,strcpy2a, or the originalstrcpy will save you ifdest points to a region of memory that isn’t big enough to hold the string atsrc, or if somebody forget to tack a null on the end ofsrc (in which casestrcpy will just keep going until it finds a null character somewhere). As elsewhere, it’s your job as a programmer to make sure there is enough room. Since the compiler has no idea whatdest points to, this means that you have to remember how much room is available there yourself.
If you are worried about overrunningdest, you could usestrncpy instead. Thestrncpy function takes a third argument that gives the maximum number of characters to copy; however, ifsrc doesn’t contain a null character in this range, the resulting string indest won’t either. Usually the only practical application tostrncpy is to extract the firstk characters of a string, as in
/* copy the substring of src consisting of characters at positions start..end-1 (inclusive) into dest *//* If end-1 is past the end of src, copies only as many characters as available. *//* If start is past the end of src, the results are unpredictable. *//* Returns a pointer to dest */char *copySubstring(char *dest,constchar *src,int start,int end){/* copy the substring */ strncpy(dest, src + start, end - start);/* add null since strncpy probably didn't */ dest[end - start] = '\0';return dest;}Another quick and dirty way to extract a substring of a string you don’t care about (and can write to) is to just drop a null character in the middle of the sacrificial string. This is generally a bad idea unless you are certain you aren’t going to need the original string again, but it’s a surprisingly common practice among C programmers of a certain age.
A similar operation tostrcpy isstrcat. The difference is thatstrcat concatenatessrc on to the end ofdest; so that ifdest previous pointed to"abc" andsrc to"def",dest will now point to"abcdef". Likestrcpy,strcat returns its first argument. A no-return-value version ofstrcat is given below.
Decoding this abomination is left as an exercise for the reader. There is also a functionstrncat which has the same relationship tostrcat thatstrncpy has tostrcpy.
As withstrcpy, the actual implementation ofstrcat may be much more subtle, and is likely to be faster than rolling your own.
Because the length of a string is of fundamental importance in C (e.g., when deciding if you can safely copy it somewhere else), the standard C library provides a functionstrlen that counts the number of non-null characters in a string. Note that if you are allocating space for a copy of a string, you will need to add one to the value returned bystrlen to account for the null.
Here’s a possible implementation:
Note the use of the comma operator in the increment step. The comma operator applied to two expressions evaluates both of them and discards the value of the first; it is usually used only infor loops where you want to initialize or advance more than one variable at once.
Like the other string routines, usingstrlen requires includingstring.h.
A common mistake is to put a call tostrlen in the header of a loop; for example:
/* like strcpy, but only copies characters at indices 0, 2, 4, ... from src to dest */char *copyEvenCharactersBadVersion(char *dest,constchar *src){int i;int j;/* BAD: Calls strlen on every pass through the loop */for(i =0, j =0; i < strlen(src); i +=2, j++) { dest[j] = src[i]; } dest[j] = '\0';return dest;}The problem is thatstrlen has to scan all ofsrc every time the test is done, which adds time proportional to the length ofsrc to each iteration of the loop. SocopyEvenCharactersBadVersion takes time proportional to thesquare of the length ofsrc.
Here’s a faster version:
/* like strcpy, but only copies characters at indices 0, 2, 4, ... from src to dest */char *copyEvenCharacters(char *dest,constchar *src){int i;int j;int len;/* length of src */ len = strlen(src);/* GOOD: uses cached value of strlen(src) */for(i =0, j =0; i < len; i +=2, j++) { dest[j] = src[i]; } dest[j] = '\0';return dest;}Because it doesn’t callstrlen all the time, this version ofcopyEvenCharacters will run much faster than the original even on small strings, and several million times faster ifsrc is megabytes long.
If you want to test if stringss1 ands2 contain the same characters, writings1 == s2 won’t work, since this tests instead whethers1 ands2 point to the same address. Instead, you should usestrcmp, declared instring.h. Thestrcmp function walks along both of its arguments until it either hits a null on both and returns 0, or hits two different characters, and returns a positive integer if the first string’s character is bigger and a negative integer if the second string’s character is bigger (a typical implementation will just subtract the two characters). A straightforward implementation might look like this:
intstrcmp(constchar *s1,constchar *s2){while(*s1 && *s2 && *s1 == *s2) { s1++; s2++; }return *s1 - *s2;}To usestrcmp to test equality, test if the return value is0:
You may sometimes see this idiom instead:
My own feeling is that the first version is more clear, since!strcmp always suggested to me that you were testing for the negation of some property (e.g. not equal). But if you think ofstrcmp as telling you when two strings are different rather than when they are equal, this may not be so confusing.
You can write formatted output to a string buffer withsprintf just like you can write it tostdout withprintf or to a file withfprintf. Make sure when you do so that there is enough room in the buffer you are writing to, or the usual bad things will happen.
When allocating space for a copy of a strings usingmalloc, the required space isstrlen(s)+1. Don’t forget the+1, or bad things may happen.11
Because allocating space for a copy of a string is such a common operation, many C libraries provide astrdup function that does exactly this. If you don’t have one (it’s not required by the C standard), you can write your own like this:
/* return a freshly-malloc'd copy of s *//* or 0 if malloc fails *//* It is the caller's responsibility to free the returned string when done. */char *strdup(constchar *s){char *s2; s2 = malloc(strlen(s)+1);if(s2 !=0) { strcpy(s2, s); }return s2;}Exercise: Write a functionstrcatAlloc that returns a freshly-malloc’d string that concatenates its two arguments. Exactly how many bytes do you need to allocate?
Now that we know about strings, we can finally do something withargc andargv.
Recall thatargv inmain is declared aschar **; this means that it is a pointer to a pointer to achar, or in this case the base address of an array of pointers tochar, where each such pointer references a string. These strings correspond to the command-line arguments to your program, with the program name itself appearing inargv[0]12
The countargc counts all arguments includingargv[0]; it is1 if your program is called with no arguments and larger otherwise.
Here is a program that prints its arguments. If you get confused about whatargc andargv do, feel free to compile this and play with it:
#include<stdio.h>intmain(int argc,char **argv){int i; printf("argc = %d\n\n", argc);for(i =0; i < argc; i++) {printf("argv[%d] = %s\n", i, argv[i]); }return0;}Like strings, C terminatesargv with a null: the value ofargv[argc] is always 0 (a null pointer tochar). In principle this allows you to recoverargc if you lose it.
C has two kinds of structured data types:structs andunions. Astruct holds multiple values in consecutive memory locations, calledfields, and implements what in type theory is called aproduct type: the set of possible values is the Cartesian product of the sets of possible values for its fields. In contrast, aunion has multiple fields but they are all stored in the same location: effectively, this means that only one field at a time can hold a value, making aunion asum type whose set of possible values is the union of the sets of possible values for each of its fields. Unlike what happens in more sensible programming languages,unions are not tagged: unless you keep track of this information somewhere else, you can’t tell which field in a union is being used, and you can store a value of one type in aunion and try to read it back as a different type, and C won’t complain.13
Astruct is a way to define a type that consists of one or more other types pasted together. Here’s a typicalstruct definition:
This defines a new typestruct string that can be used anywhere you would use a simple type likeint orfloat. When you declare a variable with typestruct string, the compiler allocates enough space to hold both anint and achar *.
Note that this declaration has a semicolon after the close brace. This can be confusing, since most close braces in C appear at the end of function bodies or compound statements, and are not followed by a semicolon. If you get strange error messages for lines following astruct definition, it’s worth checking to make sure that the semicolon is there.
You can get at the individual components of astruct using the. operator, like this:
struct string {int length;char *data;};intmain(int argc,char **argv){struct string s; s.length =4; s.data ="this string is a lot longer than you think"; puts(s.data);return0;}Variables of typestruct can be assigned to, passed into functions, returned from functions, just like any other type. Each such operation is applied componentwise; for example,s1 = s2; is equivalent tos1.length = s2.length; s1.data = s2.data;.
These operations are not used as often as you might think: typically, instead of copying around entire structures, C programs pass around pointers, as is done with arrays. Pointers tostructs are common enough in C that a special syntax is provided for dereferencing them.14 Suppose we have:
struct string s;/* a struct */struct string *sp;/* a pointer to a struct */ s.length =4; s.data ="another overly long string"; sp = &s;/* sp now points to s */We can then refer to elements of thestruct string thatsp points to (i.e. s) in either of two ways:
The second is more common, since it involves typing fewer parentheses. It is an error to write*sp.data in this case; since. binds tighter than*, the compiler will attempt to evaluatesp.data first and generate an error, sincesp doesn’t have adata field.
Pointers tostructs are commonly used in definingabstract data types, since it is possible to declare that a function returns e.g. astruct string * without specifying the components of astruct string. (All pointers tostructs in C have the same size and structure, so the compiler doesn’t need to know the components to pass around the address.) Hiding the components discourages code that shouldn’t look at them from doing so, and can be used, for example, to enforce consistency between fields.
For example, suppose we wanted to define astruct string * type that held counted strings that could only be accessed through a restricted interface that prevented (for example) the user from changing the string or its length. We might create a filemyString.h that contained the declarations:
/* make a struct string * that holds a copy of s *//* returns 0 if malloc fails */struct string *makeString(constchar *s);/* destroy a struct string * */void destroyString(struct string *);/* return the length of a struct string * */int stringLength(struct string *);/* return the character at position index in the struct string * *//* or returns -1 if index is out of bounds */int stringCharAt(struct string *s,int index);and then the actual implementation inmyString.c would be the only place where the components of astruct string were defined:
#include<stdlib.h>#include<string.h>#include"myString.h"struct string {int length;char *data;};struct string *makeString(constchar *s){struct string *s2; s2 = malloc(sizeof(struct string));if(s2 ==0) {return0; }/* let caller worry about malloc failures */ s2->length = strlen(s); s2->data = malloc(s2->length);if(s2->data ==0) {free(s2);return0; } strncpy(s2->data, s, s2->length);return s2;}voiddestroyString(struct string *s){ free(s->data); free(s);}intstringLength(struct string *s){return s->length;}intstringCharAt(struct string *s,int index){if(index <0 || index >= s->length) {return -1; }else {return s->data[index]; }}In practice, we would probably go even further and replace all thestruct string * types with a new name declared withtypedef.
What you can do to structs is pretty limited: you can look up or set individual components in a struct, you can pass structs to functions or as return values from functions (which makes a copy of the original struct), and you can assign the contents of one struct to another usings1 = s2 (which is equivalent to copying each component separately).
One thing that youcan’t do is test two structs for equality using==; this is because structs may contain extra space holding junk data. If you want to test for equality, you will need to do it componenti by component.
The C99 standard guarantees that the components of astruct are stored in memory in the same order that they are defined in: that is, later components are placed at higher address. This allows sneaky tricks like truncating a structure if you don’t use all of its components. Because ofalignment restrictions, the compiler may add padding between components to put each component on its prefered alignment boundary.
You can find the position of a component within astruct using theoffsetof macro, which is defined instddef.h. This returns the number of bytes from the base of the struct that the component starts at, and can be used to do various terrifying non-semantic things with pointers.
#include<stdio.h>#include<stdlib.h>#include<stddef.h>#include<assert.h>intmain(int argc,char **argv){struct foo {int i;char c;double d;float f;char *s; }; printf("i is at %lu\n", offsetof(struct foo, i)); printf("c is at %lu\n", offsetof(struct foo, c)); printf("d is at %lu\n", offsetof(struct foo, d)); printf("f is at %lu\n", offsetof(struct foo, f)); printf("s is at %lu\n", offsetof(struct foo, s));return0;}It is possible to specify the exact number of bits taken up by a member of astruct of integer type. This is seldom useful, but may in principle let you pack more information in less space. Bit fields are sometimes used to unpack data from an external source that uses this trick, but this is dangerous, because there is no guarantee that the compiler will order the bit fields in yourstruct in any particular order (at the very least, you will need to worry aboutendianness.
Example:
This defines astruct that (probably) occupies only one byte, and supplies four 2-bit fields, each of which can hold values in the range 0-3.
Aunion is just like astruct, except that instead of allocating space to store all the components, the compiler only allocates space to store the largest one, and makes all the components refer to the same address. This can be used to save space if you know that only one of several components will be meaningful for a particular object. An example might be a type representing an object in a LISP-like language like Scheme:
struct lispObject {int type;/* type code */union {int intVal;double floatVal;char * stringVal;struct {struct lispObject *car;struct lispObject *cdr; } consVal; } u;};Now if you wanted to make astruct lispObject that held an integer value, you might write
HereTYPE_INT has presumably been defined somewhere. Note that nothing then prevents you from writing
The effects of this will be strange, since it’s likely that the bit pattern representing 27 as anint represents something very different as adouble. Avoiding such mistakes is your responsibility, which is why most uses ofunion occur inside largerstructs that contain enough information to figure out which variant of theunion applies.
C provides theenum construction for the special case where you want to have a sequence of named constants of typeint, but you don’t care what their actual values are, as in
This will assign the value0 toRED,1 toBLUE, and so on. These values are effectively of typeint, although you can declare variables, arguments, and return values as typeenum color to indicate their intended interpretation.
Despite declaring a variableenum color c (say), the compiler will still allowc to hold arbitrary values of typeint.
So the following ridiculous code works just fine:
#include<stdio.h>#include<stdlib.h>enum foo { FOO };enum apple { MACINTOSH, CORTLAND, RED_DELICIOUS };enum orange { NAVEL, CLEMENTINE, TANGERINE };intmain(int argc,char **argv){enum foo x;if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; } printf("FOO = %d\n", FOO); printf("sizeof(enum foo) = %d\n",sizeof(enum foo)); x =127; printf("x = %d\n", x);/* note we can add apples and oranges */ printf("%d\n", RED_DELICIOUS + TANGERINE);return0;}It is also possible to specify particular values for particular enumerated constants, as in
Anything that doesn’t get a value starts with one plus the previous value; so the above definition would setTURQUOISE to6. This may result in two names mapping to the same value.
In practice,enums are seldom used, and you will more commonly see a stack of#defines:
The reason for this is partly historical—enum arrived late in the evolution of C—but partly practical: a table of#defines makes it much easier to figure out which color is represented by 3, without having to count through a list. But if you never plan to use the numerical values,enum may be a better choice, because it guarantees that all the values will be distinct.
enum withunionA natural place to use anenum is to tag aunion with the type being used. For example, a Lisp-like language might implement the following multi-purpose data type:
enum TypeCode { TYPE_INT, TYPE_DOUBLE, TYPE_STRING };struct LispValue {enum TypeCode typeCode;union {int i;double d;char *s; } value;};Here we don’t care what the numeric values ofTYPE_INT,TYPE_DOUBLE, andTYPE_STRING are, as long as we can applyswitch totypeCode to figure out what to do with one of these things.
typedefSuppose that you want to represent character strings as
If you later change the representation to, say, traditional null-terminatedchar * strings or some even more complicated type (union string **some_string[2];), you will need to go back and replace ever occurrence ofstruct string * in every program that uses it with the new type. Even if you don’t expect to change the type, you may still get tired of typingstruct string * all the time, especially if your fingers slip and give youstruct string sometimes.
The solution is to use atypedef, which defines a new type name:
The syntax fortypedef looks like a variable declaration preceded bytypedef, except that the variable is replaced by the new type name that acts like whatever type the defined variable would have had. You can use a name defined withtypedef anywhere you could use a normal type name, as long as it is later in the source file than thetypedef definition. Typicallytypedefs are placed in a header file (.h file) that is then included anywhere that needs them.
You are not limited to usingtypedefs only for complex types. For example, if you were writing numerical code and wanted to declare overtly that a certain quantity was not just anydouble but actually a length in meters, you could write
typedefdouble LengthInMeters;typedefdouble AreaInSquareMeters;AreaInSquareMeters rectangleArea(LengthInMeters height, LengthInMeters width);Unfortunately, C does not do type enforcement ontypedef’d types: it is perfectly acceptable to the compiler if you pass a value of typeAreaInSquareMeters as the first argument torectangleArea, since by the time it checks it has replaced byAreaInSquareMeters andLengthInMeters bydouble. So this feature is not as useful as it might be, although it does mean that you can writerectangleArea(2.0, 3.0) without having to do anything to convert2.0 and3.0 to typeLengthInMeters.
There are certain cases where the compiler needs to know the definition of astruct:
structs, because they appear in a largerstruct, when you are passing thestruct as an argument or assigning it to a variable, or just because you appliedsizeof to thestruct.But the compiler doesnot need to know the definition of astruct to know how create a pointer to it. This is because allstruct pointers have the same size and structure.
This allows a trick called anopaque struct, which can be used forinformation hiding, where one part of your program is allowed to see the definition of astruct but other parts are not.
The idea is to create a header file that defines all the functions that might be used to access thestruct, but does not define thestruct itself. For example, suppose we want to create a counter, where the user can call a functionincrement that acts like++ in the sense that it increments the counter and returns the new value, but we don’t want to allow the user to change the value of the counter in any other way. This header file defines theinterface to the counter.
Here is the header file:
/* Create a new counter, initialized to 0. Call counterDestroy to get rid of it. */struct counter * counterCreate(void);/* Free space used by a counter. */void counterDestroy(struct counter *);/* Increment a counter and return new value. */int counterIncrement(struct counter *);We can now write code that uses thestruct counter * type without knowing what it is actually pointing to:
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include"counter.h"intmain(int argc,char **argv){struct counter *c;int value; c = counterCreate();while((value = counterIncrement(c)) <10) { printf("%d\n", value); } counterDestroy(c);return0;}To make this work, we do have to provide animplementation. The obvious way to do it is have astruct counter store the counter value in anint, but one could imagine other (probably bad) implementations that did other things, as long as from the outside they acted like we expect.
We only put the definition of astruct counter in this file. This means that only functions in this file can access a counter’s components, compute the size of a counter, and so forth. While we can’t absolutely prevent some other function from extracting or modifying the contents of a counter (C doesn’t provide that kind of memory protection), we can at least hint very strongly that the programmer shouldn’t be doing this.
#include<stdlib.h>#include<assert.h>#include"counter.h"struct counter {int value;};struct counter *counterCreate(void){struct counter *c; c = malloc(sizeof(struct counter)); assert(c); c->value =0;return c;}voidcounterDestroy(struct counter *c){ free(c);}intcounterIncrement(struct counter *c){return ++(c->value);}We will see this trick used over and over again when we buildabstract data types.
See K&R Appendix A12.3 for full details on macro expansion in ANSI C andhttp://gcc.gnu.org/onlinedocs/cpp/Macros.html for documentation on whatgcc supports.
The short version: the command
causes any occurrence of the wordFOO in your source file to be replaced by(12) by the preprocessor. To count as a word,FOO can’t be adjacent to other alphanumeric characters, so for exampleFOOD willnot expand to(12)D.
To create a macro with arguments, put them in parentheses separated by commas after the macro name, e.g.
Now if you writeSquare(foo) it will expand as((foo)*(foo)). Note the heavy use of parentheses inside the macro definition to avoid trouble with operator precedence; if instead we had written
thenBadSquare(3+4) would give3+4*3+4, which evaluates to19, which is probably not what we intended. The general rule is that macro arguments should always be put in parentheses if you are using them in an expression where precedence might be an issue.
You can have multiple arguments to a macro, e.g.
The usual caveats about using lots of parentheses apply.
Macros can have odd effects if their arguments perform side-effects. For example,Square(++x) expands to((++x)*(++x)); ifx starts out equal to1, this expression may evaluate to any of2,6, or9 depending on when the++ operators are evaluated, and will definitely leave3 inx instead of the2 the programmer probably expects. For this reason it is generally best to avoid side-effects in macro arguments, and to mark macro names (e.g. by capitalization) to clearly distinguish them from function names, where this issue doesn’t come up.
C99 addedvariadic macros that may have a variable number of arguments; these are mostly useful for dealing with variadic functions (likeprintf) that also take a variable number of arguments.
To define a variadic macro, define a macro with arguments where the last argument is three periods:... . The macro__VA_ARGS__ then expands to whatever arguments matched this ellipsis in the macro call.
For example:
#include<stdio.h>#define Warning(...) fprintf(stderr, __VA_ARGS__)intmain(int argc,char **argv){ Warning("%s: this program contains no useful code\n", argv[0]);return1;}It is possible to mix regular arguments with..., as long as... comes last:
It is sometimes tempting to use a macro to avoid having to retype some small piece of code that does not seem big enough to justify a full-blown function, especially if the cost of the body of the function is small relative to the cost of a function call.Inline functions are a mechanism that is standard in C99 (and found in some compilers for older variants of C) that give you the ability to write a function that will never pay this function call overhead; instead, any call to an inline function is effectively replaced by the body of the function. Unlike parameterized macros, inline functions do not suffer from issues with duplicated parameters or weird text-substitution oddities.
To take a simple example, thedistSquared function that we used to illustratefunction definitions doesn’t do very much: just two multiplications and an addition. If we are doing a lot ofdistSquared computations, we could easily double the cost of each computation with function call overhead. One alternative might be to use a macro:
but this suffers from the parameter-duplication problem, which could be particularly unfortunate if we computeDistSquared(expensiveFunctionWithManySideEffects(), 12). A better alternative is to use an inline function.
Like macros, inline functions should be defined in header files. Ordinary functions always go in C files because (a) we only want to compile them once, and (b) the linker will find them in whatever.o file they end up in anyway. But inline functions generally don’t get compiled independently, so this doesn’t apply.
Here is a header file for an inline version ofdistSquared:
/* Returns the square of the distance between two points separated by dx in the x direction and dy in the y direction. */staticinlineintdistSquared(int dx,int dy){return dx*dx + dy*dy;}This looks exactly like the originaldistSquared, except that we addedstatic inline. We want this function to be declaredstatic because otherwise some compilers will try to emit a non-inline definition for it in ever C file this header is included in, which could have bad results.15
The nice thing about this approach is that if we do decide to makedistSquared an ordinary function (maybe it will make debugging easier, or we realize we want to be able to take its address), then we can just move the definition into a.c file and take thestatic inline off. Indeed, this is probably the safest thing to start with, since we can also do the reverse if we find that function call overhead on this particular function really does account for a non-trivial part of our running time (seeprofiling).
One macro can expand to another; for example, after defining
it will be the case thatFOO will expand toBAR which will then expand to(12). For obvious reasons, it is a bad idea to have a macro expansion contain the original macro name.
Some standard idioms have evolved over the years to deal with issues that come up in defining complex macros. Usually, having a complex macro is a sign of bad design, but these tools can be useful in some situations.
Use the comma operator, e.g.
The comma operator evaluates both of its operands and returns the value of the one on the right-hand side.
You can also choose between alternatives using the ternary?: operator, as in
(but see the warning about repeated parameters above).
Suppose you get tired of writing
all the time. In principle, you can write a macro
and then write
in place of your formerfor loop headers. This is generally a good way to make your code completely unreadable. Such macros are callednon-syntactic because they allow code that doesn’t look like syntactically correct C.
Sometimes, however, it makes sense to use non-syntactic macros when you want something that writes to a variable without having to pass it to a function as a pointer. An example might be something like thismalloc wrapper:
(Strictly speaking, this is probably more of a “non-semantic” macro.)
Whether the confusion of having a non-syntactic macro is worth the gain in safety or code-writing speed is a judgment call that can only be made after long and painful experience. If in doubt, it’s probably best not to do it.
If you want to write a macro that looks like a function call but contains multiple statements, the correct way to do it is like
This can safely be used in place of single statements, like this:16
Note that no construct exceptdo..while will work here. Just using braces will cause trouble with the semicolon before theelse, and no other compound statement besidesdo..while expects to be followed by a semicolon in this way.
Let’s rewriteNoisyInc to include the variable name:
Will this do what we want? No. The C preprocessor is smart enough not to expand macro parameters inside strings, soBadNoisyInc2(y) will expand to(puts("Incrementing x"), y++). Instead, we have to write
Here#x expands to whatever the value ofx is wrapped in double quotes. The resulting string constant is then concatenated with the adjacent string constant according to standard C string constant concatenation rules.
To concatenate things that aren’t strings, use the## operator, as in
This lets you writeFakeArray(12) instead offakeArrayVariableNumber12. Note that there is generally no good reason to ever do this.
Where this feature does become useful is if you want to be able to refer to part of the source code of your program. For example, here is short program that includes a macro that prints the source code and value of an expression:
#include<stdio.h>#define PrintExpr(x) (printf("%s = %d\n", #x, (x)))intmain(int argc,char **argv){ PrintExpr(2+2);return0;}When run, this program prints
2+2 = 4Without using a macro, there is no way to capture the text string"2+2" so we can print it.
This sort of trickery is mostly used in debugging. Theassert macro is a more sophisticated version, which uses the built-in macros__FILE__ (which expands to the current source file as a quoted string) and__LINE__ (which expands to the current source line number, not quoted) to not only print out an offending expression, but also the location of it in the source.
Nothing restricts a macro expansion to a single line, although you must put a backslash at the end of each line to keep it going. Here is a macro that declares a specialized sorting routine for any type that supports<:
#define DeclareSort(prefix, type) \static int \_DeclareSort_ ## prefix ## _Compare(const void *a, const void *b) \{ \ const type *aa; const type *bb; \ aa = a; bb = b; \ if(*aa < *bb) return -1; \ else if(*bb < *aa) return 1; \ else return 0; \} \\void \prefix ## _sort(type *a, int n)\{ \ qsort(a, n, sizeof(type), _DeclareSort_ ## prefix ## _Compare); \}A typical use might be
#include<stdio.h>#include<stdlib.h>#include"declareSort.h"/* note: must appear outside of any function, and has no trailing semicolon */DeclareSort(int,int)#define N (50)intmain(int argc,char **argv){int a[N];int i;for(i=0; i < N; i++) { a[i] = N-i; } int_sort(a, N);for(i=0; i < N; i++) { printf("%d ", a[i]); } putchar('\n');return0;}Do this too much and you will end up reinventing C++ templates, which are a more or less equivalent mechanism for generating polymorphic code that improve on C macros like the one above by letting you omit the backslashes.
In addition to generating code, macros can be used forconditional compiliation, where a section of the source code is included only if a particular macro is defined. This is done using the#ifdef and#ifndef preprocessor directives. In its simplest form, writing#ifdef NAME includes all code up to the next#endif if and only ifNAME is defined. Similarly,#ifndef NAME includes all code up to the next#endif if and only ifNAME isnot defined.
Like regular Cif statements,#ifdef and#ifndef directives can be nested, and can include else cases, which are separated by an#else directive.
#include<stdio.h>#include<assert.h>intmain(int argc,char **argv){#ifdef SAY_HI puts("Hi.");#else/* matches #ifdef SAY_HI */#ifndef BE_POLITE puts("Go away!");#else/* matches #ifndef BE_POLITE */ puts("I'm sorry, I don't feel like talking today.");#endif/* matches #ifndef BE_POLITE */#endif/* matches #ifdfe SAY_HI */#ifdef DEBUG_ARITHMETIC assert(2+2 ==5);#endifreturn0;}You can turn these conditional compilation directives on and off at compile time by passing the-D flag togcc. Here is the program above, running after compiling with different choices of options:
$ gcc -DSAY_HI -o ifdef ifdef.c$ ./ifdefHi.$ gcc -DBE_POLITE -DDEBUG_ARITHMETIC -o ifdef ifdef.c$ ./ifdefI'm sorry, I don't feel like talking today.ifdef: ifdef.c:18: main: Assertion `2+2 == 5' failed.AbortedAn example of how this mechanism can be useful is theNDEBUG macro: if you define this before includingassert.h, it turns everyassert in your code into a no-op. This can be handy if you are pretty sure your code works and you want to speed it up in its final shipped version, or if you are pretty sure your code doesn’t work but you want to hide the evidence. (It also means you should not perform side-effects inside anassert unless you are happy with them not happening.)
Using the flag-DNAME definesNAME to be1. If you want something else, use-DNAME=VALUE. This can be used to bake useful information into your program at compile time, and is often used to specify filenames. Below is a simple example.
$ gcc -DMESSAGE='"Hi there!"' -o message message.c$ ./messageHi there!Note that we had to put an extra layer of single quotes in the command line to keep the shell from stripping off the double quotes. This is unavoidable: had we writtenputs("MESSAGE") in the code, the preprocessor would have recognized thatMESSAGE appeared inside a string and would not have replaced it.17
#if directiveThe preprocessor also includes a more general#if directive that evaluates simple arithmetic expressions. The limitations are that it can only do integer arithmetic (using the widest signed integer type available to the compiler) and can only do it to integer and character constants and the special operatordefined(NAME), which evaluates to 1 ifNAME is defined and 0 otherwise. The most common use of this is to combine several#ifdef-like tests into one:
#include<stdio.h>intmain(int argc,char **argv){#if VERBOSITY >= 3 && defined(SAY_HI) puts("Hi!");#endifreturn0;}One problem with using a lot of macros is that you can end up with no idea what input is actually fed to the compiler after the preprocessor is done with it. You can tellgcc to tell you how everything expands usinggcc -E source_file.c. If your source file contains any#include statements it is probably a good idea to send the output ofgcc -E to a file so you can scroll down past the thousands of lines of text they may generate.
E.g., can you write something like
The answer isno. C preprocessor commands are only recognized in unexpanded text. If you want self-modifying macros you will need to use a fancier macro processor likem4.
Up until this point we have mostly concentrated on the details of the C programming language. In this part of the notes, we will be looking more at how to construct data structures and how to organize a program. In principle, these techniques can be applied to any programming language that supports the appropriate low-level data types, but we will continue to emphasize issues involved with implementation in C.
Asymptotic notation is a tool for measuring the growth rate of functions, which for program design usually means the way in which the time or space costs of a program scale with the size of the input. We’ll start with an example of why this is important.
Suppose we want to sort in increasing order a deck ofn cards, numbered1 throughn. Here are two algorithms for doing this.
In themergesort algorithm, we start withn piles of one card each. We then take pairs of piles and merge them together, by repeatedly pulling the smaller of the two smallest cards off the top of the pile and putting it on the bottom of our output pile. After the first round of this, we haven/2 piles of two cards each. After another round,n/4 piles of four cards each, and so on until we get one pile withn cards after roughlylog2n rounds of merging.
Here’s a picture of this algorithm in action on 8 cards:
5 7 1 2 3 4 8 657 12 34 681257 346812345678Suppose that we want to estimate the cost of this algorithm without actually coding it up. We might observe that each time a card is merged into a new pile, we need to do some small, fixed number of operations to decide that it’s the smaller card, and then do an additional small, fixed number of operations to physically move it to a new place. If we are really clever, we might notice that since the size of the pile a card is in doubles with each round, there can be at most⌈log2n⌉ rounds until all cards are in the same pile. So the cost of getting a single card in the right place will be at mostclog n wherec counts the “small, fixed” number of operations that we keep mentioning, and the cost of getting every card in the right place will be at mostcnlog n.
In the ’‘’selection sort’’’ algorithm, we look through all the cards to find the smallest one, swap it to the beginning of the list, then look through the remaining cards for the second smallest, swap it to the next position, and so on.
Here’s a picture of this algorithm in action on 8 cards:
5712348617523486125734861237548612345786123457861234568712345678This is a simpler algorithm to implement that mergesort, but it is usually slower on large inputs. We can formalize this by arguing that each time we scank cards to find the smallest, it’s going to take some small, fixed number of operations to test each card against the best one we found so far, and an additional small, fixed number of operations to swap the smallest card to the right place. To compute the total cost we have to add these costs for all cards, which will give us a total cost that looks something like(c1n + c2) + (c1(n − 1) + c2) + (c1(n − 2) + c2) + … + (c11 + c2) = c1n(n + 1)/2 + c2n.
For largen, it looks like this is going to cost more than mergesort. But how can we make this claim cleanly, particularly if we don’t know the exact values ofc,c1, andc2?
The idea is to replace complex running time formulae likecnlog n orc1n(n + 1)/2 + c2n with an asymptotic growth rateO(nlog n) orO(n2). These asymptotic growth rates omit the specific details of exactly how fast our algorithms run (which we don’t necessarily know without actually coding them up) and concentrate solely on how the cost scales as the size of the inputn becomes large.
This avoids two issues:
The idea of ’‘’asymptotic notation’’’ is to consider the shape of the worst-case costT(n) to process an input of sizen. Here, worst-case means we consider the input that gives the greatest cost, where cost is usually time, but may be something else like space. To formalize the notion of shape, we define classes of functions that behave like particular interesting functions for large inputs. The definition looks much like a limit in calculus:
Iff(n) is inO(g(n)) we sayf(n) is ’‘’big-O’’’ ofg(n) or justf(n) = O(g(n)).18
Unpacked, this definition says thatf(n) is less than a constant timesg(n) whenn is large enough.
Some examples:
Writing proofs like this over and over again is a nuisance, so we can use some basic rules of thumb to reduce messy functionsf(n) to their asymptotic forms:
This means that almost any asymptotic bound can be reduced down to one of a very small list of common bounds. Ones that you will typically see in practical algorithms, listed in increasing order, areO(1),O(log n),O(n),O(nlog n), orO(n2).
Applying these rules to mergesort and selection sort gives us asymptotic bounds ofcnlog n = O(nlog n) (the constant vanishes) andc1n(n + 1)/2 + c2n = c1n2/2 + c1n/2 + c2n = O(n2) + O(n) + O(n) = O(n2) (the constants vanish and thenO(n2) dominates). Here we see that no matter how fast our machine is at different low-level operations, for large enough inputs mergesort will beat selection sort.
To compute the asymptotic cost of a program, the rule of thumb is that any simple statement costsO(1) time to evaluate, and larger costs are the result of loops or calls to expensive functions, where a loop multiplies the cost by the number of iterations in the loop. When adding costs together, the biggest cost wins:
So this function takesO(1) time:
But this function, which computes exactly the same value, takesO(n) time:
/* return the sum of the integers i with 0 <= i and i < n */intsumTo(int n){int i;int sum =0;for(i =0; i < n; i++) { sum += i; }return sum;}The reason it takes so long is that each iteration of the loop takes onlyO(1) time, but we execute the loopn times, andn ⋅ O(1) = O(n).
Here’s an even worse version that takesO(n2) time:
/* return the sum of the integers i with 0 <= i and i < n */intsumTo(int n){int i;int j;int sum =0;for(i =0; i < n; i++) {for(j =0; j < i; j++) { sum++; } }return sum;}Here we have two nested loops. The outer loop iterates exactlyn times, and for each iteration the inner loop iterates at mostn times, and the innermost iteration costsO(1) each time, so the total is at mostO(n2). (In fact, it’s no better than this, because at leastn/2 times we execute the inner loop, we do at leastn/2 iterations.)
So even if we knew that the constant on the first implementation was really large (maybe our CPU is bad at dividing by 2?), for big values ofn it’s still likely to be faster than the other two.
(This example is a little misleading, becausen is not the size of the input but the actual input value. More typical might be a statement that the cost ofstrlen isO(n) wheren is the length of the string.)
Big-O notation is good for upper bounds, but the inequality in the definition means that it can’t be used for anything else: it is the case that12 = O(n67) just because12 < n67 whenn is large enough. There is an alternative definition, called ’‘’big-Omega’’’, that works in the other direction:
This is exactly the same as the definition ofO(g(n)) except that the inequality goes in the other direction. So if we want to express that some algorithm is very expensive, we might write that it’sΩ(n2), which says that once the size of the input is big enough, then the cost grows at least as fast asn2.
If you want to claim that your bound istight—both an upper and a lower bound—usebig-Theta:f(n) isΘ(g(n)) if it is bothO(f(n)) andΩ(g(n)).
Mostly we will just use big-O, with the understanding that when we say that a particular algorithm isO(n), that’s the best bound we could come up with.
Standard C arrays have fixed size, but for many applications, we’d like to be able to grow or shrink an array as needed. This can be done efficiently with careful use ofrealloc. The resulting data structure is called adynamic array and includes enough information beyond the contents of the array itself to allow resizing to be handled mostly automatically.
The pointer returned bymalloc doesn’t give us access to the size of the block, so we are going to need to keep track of this for ourselves. We can do this with astruct that holds a pointer to the block and the size as fields. For some applications, it may be convenient to keep track of a separate count of the number of locations we actually use. This allows use to increase the size of the block to more than we are asked for, so we don’t have to resize every time the user wants to expand it. The usual strategy is that when we grow the block, we guarantee that its new size is at least twice its old size. This gives a trade-off between wasting space (by allocating a block that is up to twice as big as what the user asked for) and wasting time (doubling means that by the time we allocate anothern elements, we’ve increased the size of the array by at leastn, so the cost per element isO(1)).
A typicalstruct implementing a dynamic array might look like this:
struct array {size_t n;// number of elements usedsize_t size;// size of data blockint *data;// data block};The functions implementing the array will be responsible for maintaining the invariants thatn <= size andsize is in fact the number ofints that can be stored indata.
Below is a simplified implementation of a dynamic array ofints. This implementation skips then component by not allowing the user to specify the size of the array. Instead, the providedarrayGet andarraySet functions automatically resize the array so that any position the user asks for will be available. This makes the array effectively infinite from the user’s perspective, although available memory may eventually put a constraint on how infinite the array is in practice.
// Basic dynamic array type.//// This acts like an unboundedly large array// of ints, all initialized to zero.//// The trick is to resize the underlying array// whenever the caller asks for a location that// isn't in the array already.// Create a new array initialized to 0.struct array *arrayCreate(void);// Return the value in an array at given position.int arrayGet(struct array *,size_t position);// Set the value in an array at given position.// Returns new value.int arraySet(struct array *,size_t position,int value);// Free all space used by an array.void arrayDestroy(struct array *);#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include<string.h>#include"array.h"// Invariant: size == number of elements in datastruct array {size_t size;// number of elements in data;int *data;// malloc'd block};#define SIZE_INITIAL (16)#define SIZE_MULTIPLIER (2)// Minimum ratio to grow by.// Create a new array initialized to 0.struct array *arrayCreate(void){struct array *a = malloc(sizeof(struct array)); assert(a); a->size = SIZE_INITIAL;// calloc zeroes out the array a->data = calloc(a->size,sizeof(int)); assert(a->data);return a;}// Expand array if needed to include position.staticvoidarrayExpand(struct array *a,size_t position){if(position >= a->size) {// expansion neededsize_t bigger = a->size * SIZE_MULTIPLIER;if(position >= bigger) {// even more expansion needed bigger = position +1; }// Breaks invariant, we will fix later// after using a->size to zero out new space. a->data = realloc(a->data,sizeof(int) * bigger); memset(&a->data[a->size],0,sizeof(int) * (bigger - a->size)); a->size = bigger; }}// Return the value in an array at given position.intarrayGet(struct array *a,size_t position){if(position >= a->size) {// No need to expand, just return the default value.return0; }else {return a->data[position]; }}// Set the value in an array at given position.intarraySet(struct array *a,size_t position,int value){ arrayExpand(a, position);return a->data[position] = value;}// Free all space used by an array.voidarrayDestroy(struct array *a){ free(a->data); free(a);}Here is a simple test programtestArray.c andMakefile.
Recursion is when a function calls itself. Some programming languages (particularly functional programming languages likeScheme,ML, orHaskell use recursion as a basic tool for implementing algorithms that in other languages would typically be expressed usingiteration (loops). Procedural languages like C tend to emphasize iteration over recursion, but can support recursion as well.
Here are a bunch of routines that print the numbers from 0 to 9:
#include<stdio.h>#include<stdlib.h>#include<assert.h>/* all of these routines print numbers i where start <= i < stop */voidprintRangeIterative(int start,int stop){int i;for(i = start; i < stop; i++) { printf("%d\n", i); }}voidprintRangeRecursive(int start,int stop){if(start < stop) { printf("%d\n", start); printRangeRecursive(start+1, stop); }}voidprintRangeRecursiveReversed(int start,int stop){if(start < stop) { printRangeRecursiveReversed(start+1, stop); printf("%d\n", start); }}voidprintRangeRecursiveSplit(int start,int stop){int mid;if(start < stop) { mid = (start + stop) /2; printRangeRecursiveSplit(start, mid); printf("%d\n", mid); printRangeRecursiveSplit(mid+1, stop); }}#define Noisy(x) (puts(#x), x)intmain(int argc,char **argv){if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; } Noisy(printRangeIterative(0,10)); Noisy(printRangeRecursive(0,10)); Noisy(printRangeRecursiveReversed(0,10)); Noisy(printRangeRecursiveSplit(0,10));return0;}And here is the output:
printRangeIterative(0, 10)0123456789printRangeRecursive(0, 10)0123456789printRangeRecursiveReversed(0, 10)9876543210printRangeRecursiveSplit(0, 10)0123456789The first functionprintRangeIterative is simple and direct: it’s what we’ve been doing to get loops forever. The others are a bit more mysterious.
The functionprintRangeRecursive is an example of solving a problem using adivide and conquer approach. If we don’t know how to print a range of numbers 0 through 9, maybe we can start by solving a simpler problem of printing the first number 0. Having done that, we have a new, smaller problem: print the numbers 1 through 9. But then we notice we already have a functionprintRangeRecursive that will do that for us. So we’ll call it.
If you aren’t used to this, it has the feeling of trying to make yourself fly by pulling very hard on your shoelaces.19 But in fact the computer will happily generate the eleven nested instances ofprintRangeRecursive to make this happen. When we hit the bottom, the call stack will look something like this:
printRangeRecursive(0, 10) printRangeRecursive(1, 10) printRangeRecursive(2, 10) printRangeRecursive(3, 10) printRangeRecursive(4, 10) printRangeRecursive(5, 10) printRangeRecursive(6, 10) printRangeRecursive(7, 10) printRangeRecursive(8, 10) printRangeRecursive(9, 10) printRangeRecursive(10, 10)This works because each call toprintRangeRecursive gets its own parameters and its own variables separate from the others, even the ones that are still in progress. So each will print outstart and then call another copy in to printstart+1 etc. In the last call, we finally fail the teststart < stop, so the function exits, then its parent exits, and so on until we unwind all the calls on the stack back to the first one.
InprintRangeRecursiveReversed, the calling pattern is exactly the same, but now instead of printingstart on the way down, we printstart on the way back up, after making the recursive call. This means that inprintRangeRecursiveReversed(0, 10), 0 is printed only after the results ofprintRangeRecursiveReversed(1, 10), which gives us the countdown effect.
So far these procedures all behave very much like ordinary loops, with increasing values on the stack standing in for the loop variable. More exciting isprintRangeRecursiveSplit. This function takes a much more aggressive approach to dividing up the problem: it splits a range[0, 10) as two ranges[0, 5) and[6, 10) separated by a midpoint5. (The notation[x, y) means all numbersz such thatx ≤ z < y.) We want to print the midpoint in the middle, of course, and we can useprintRangeRecursiveSplit recursively to print the two ranges. Following the execution of this procedure is more complicated, with the start of the sequence of calls looking something like this:
printRangeRecursiveSplit(0, 10) printRangeRecursiveSplit(0, 5) printRangeRecursiveSplit(0, 2) printRangeRecursiveSplit(0, 1) printRangeRecursiveSplit(0, 0) printRangeRecursiveSplit(1, 1) printRangeRecursiveSplit(2, 2) printRangeRecursiveSplit(3, 5) printRangeRecursiveSplit(3, 4) printRangeRecursiveSplit(3, 3) printRangeRecursiveSplit(4, 4) printRangeRecursiveSplit(5, 5) printRangeRecursiveSplit(6, 10) ... etc.Here the computation has the structure of a tree instead of a list, so it is not so obvious how one might rewrite this procedure as a loop.
Like iteration, recursion is a powerful tool that can cause your program to do much more than expected. While it may seem that errors in recursive functions would be harder to track down than errors in loops, most of the time there are a few basic causes.
Suppose we leave out theif statement inprintRangeRecursive:
voidprintRangeRecursiveBad(int start,int stop){ printf("%d\n", start); printRangeRecursiveBad(start+1, stop);}This will still work, in a sense. When called asprintRangeRecursiveBad(0, 10), it will print 0, call itself withprintRangeRecursiveBad(1, 10), print 1, 2, 3, etc., but there is nothing to stop it at 10 (or anywhere else). So our output will be a long string of numbers, followed by a segmentation fault when we blow out the stack.
This is the recursive version of an infinite loop: the same thing happens if we forget a loop test and write
except that now the program just runs forever, since it never runs out of resources. This is an example of how iteration is more efficient than recursion, at least in C.
Blowing out the stack is what happens when a recursion is too deep. Typically, the operating system puts a hard limit on how big the stack can grow, on the assumption that any program that grows the stack too much has gone wrong and needs to be stopped before it does more damage. One of the ways this can happen is if we forget the base case as above, but it can also happen if we just try to use a recursive function to do too much. For example, if we callprintRangeRecursive(0, 1000000), we will probably get a segmentation fault after the first 100,000 numbers or so.
For this reason, it’s best to try to avoid linear recursions like the one inprintRangeRecursive, where the depth of the recursion is proportional to the number of things we are doing. Much safer are even splits likeprintRangeRecursiveSplit, since the depth of the stack will now be only logarithmic in the number of things we are doing. Fortunately, linear recursions are oftentail-recursive, where the recursive call is the last thing the recursive function does; in this case, we can use astandard transformation to convert the tail-recursive function into an iterative function.
Sometimes we end up blowing out the stack because we thought we were recursing on a smaller instance of the problem, but in fact we weren’t. Consider this broken version ofprintRangeRecursiveSplit:
voidprintRangeRecursiveSplitBad(int start,int stop){int mid;if(start == stop) { printf("%d\n", start); }else { mid = (start + stop) /2; printRangeRecursiveSplitBad(start, mid); printRangeRecursiveSplitBad(mid, stop); }}This will get stuck on as simple a call asprintRangeRecursiveSplitBad(0, 1); it will setmid to 0, and while the recursive call toprintRangeRecursiveSplitBad(0, 0) will work just fine, the recursive call toprintRangeRecursiveSplitBad(0, 1) will put us back where we started, giving an infinite recursion.
Detecting these errors is usually not too hard (segmentation faults that produce huge piles of stack frames when you typewhere in gdb are a dead give-away). Figuring out how to make sure that you do in fact always make progress can be trickier.
Tail recursion is when a recursive function calls itself only once, as the last thing it does. TheprintRangeRecursive function is an example of a tail-recursive function:
voidprintRangeRecursive(int start,int stop){if(start < stop) { printf("%d\n", start); printRangeRecursive(start+1, stop); }}The nice thing about tail-recursive functions is that we can always translate them directly into iterative functions. The reason is that when we do the tail call, we are effectively replacing the current copy of the function with a new copy with new arguments. So rather than keeping around the old zombie parent copy—which has no purpose other than to wait for the child to return and then return itself—we can reuse it by assigning new values to its arguments and jumping back to the top of the function.
Done literally, this produces thisgoto-considered-harmful monstrosity:
voidprintRangeRecursiveGoto(int start,int stop){ topOfFunction:if(start < stop) { printf("%d\n", start); start = start+1;goto topOfFunction; }}But we can almost always removegoto statements using less dangerous control structures. In this particular case, the pattern of jumping back to just before anif matches up exactly with what we get from awhile loop:
voidprintRangeRecursiveNoMore(int start,int stop){while(start < stop) { printf("%d\n", start); start = start+1; }}In functional programming languages, this transformation is usually done in the other direction, to unroll loops into recursive functions. Since C doesn’t like recursive functions so much (they blow out the stack!), we usually do this transformation got get rid of recursion instead of adding it.
Binary search is an algorithm for searching a sorted array for a particular target element, similar to playing Twenty Questions when the answer is a number (hopefully in a range that includes at most220 numbers). The algorithm starts by picking an value in the middle of the array. If the target is less than this value, we recurse on the bottom half of the array; else we recurse on the top half.
Here is an interface for binary search on an array ofints:
/* returns 1 if target is present in sorted array */int binarySearch(int target,constint *a,size_t length);Written recursively, we might implement the algorithm like this:
#include<stddef.h>#include"binarySearch.h"intbinarySearch(int target,constint *a,size_t length){size_t index; index = length/2;if(length ==0) {/* nothing left */return0; }elseif(target == a[index]) {/* got it */return1; }elseif(target < a[index]) {/* recurse on bottom half */return binarySearch(target, a, index); }else {/* recurse on top half *//* we throw away index+1 elements (including a[index]) */return binarySearch(target, a+index+1, length-(index+1)); }}This will work just fine, and indeed it finds the target element (or not) inO(log n) time, because we can only recurseO(log n) times before running out of elements and we only payO(1) cost per recursive call tobinarySearch. But we do have to pay function call overhead for each recursive call, and there is a potential to run into stack overflow if our stack is very constrained.
Fortunately, we don’t do anything with the return value frombinarySearch but pass it on up the stack: the function is tail-recursive. This means that we can get rid of the recursion by reusing the stack from from the initial call. The mechanical way to do this is wrap the body of the routine in afor(;;) loop (so that we jump back to the top whenever we hit the bottom), and replace each recursive call with one or more assignments to update any parameters that change in the recursive call. The result looks like this:
#include<stddef.h>#include"binarySearch.h"intbinarySearch(int target,constint *a,size_t length){size_t index;/* direct translation of recursive version *//* hence the weird organization of the loop */for(;;) { index = length/2;if(length ==0) {/* nothing left */return0; }elseif(target == a[index]) {/* got it */return1; }elseif(target < a[index]) {/* recurse on bottom half */ length = index; }else {/* recurse on top half *//* we throw away index+1 elements (including a[index]) */ a = a + index +1; length = length - (index +1); } }}Here’s some simple test code to demonstrate that these two implementations in fact do the same thing:Makefile,testBinarySearch.c.
So far the examples we have given have not been very useful, or have involved recursion that we can easily replace with iteration. Here is an example of a recursive procedure that cannot be turned into an iterative version so easily.
We are going to implement themergesort algorithm on arrays. This is a classicdivide and conquer sorting algorithm that splits an array into two pieces, sorts each piece (recursively!), then merges the results back together. Here is the code, together with a simple test program.
#include<stdio.h>#include<stdlib.h>#include<string.h>/* merge sorted arrays a1 and a2, putting result in out */voidmerge(int n1,constint a1[],int n2,constint a2[],int out[]){int i1;int i2;int iout; i1 = i2 = iout =0;while(i1 < n1 || i2 < n2) {if(i2 >= n2 || ((i1 < n1) && (a1[i1] < a2[i2]))) {/* a1[i1] exists and is smaller */ out[iout++] = a1[i1++]; }else {/* a1[i1] doesn't exist, or is bigger than a2[i2] */ out[iout++] = a2[i2++]; } }}/* sort a, putting result in out *//* we call this mergeSort to avoid conflict with mergesort in libc */voidmergeSort(int n,constint a[],int out[]){int *a1;int *a2;if(n <2) {/* 0 or 1 elements is already sorted */ memcpy(out, a,sizeof(int) * n); }else {/* sort into temp arrays */ a1 = malloc(sizeof(int) * (n/2)); a2 = malloc(sizeof(int) * (n - n/2)); mergeSort(n/2, a, a1); mergeSort(n - n/2, a + n/2, a2);/* merge results */ merge(n/2, a1, n - n/2, a2, out);/* free the temp arrays */ free(a1); free(a2); }}#define N (20)intmain(int argc,char **argv){int a[N];int b[N];int i;for(i =0; i < N; i++) { a[i] = N-i-1; }for(i =0; i < N; i++) { printf("%d ", a[i]); } putchar('\n'); mergeSort(N, a, b);for(i =0; i < N; i++) { printf("%d ", b[i]); } putchar('\n');return0;}The cost of this is pretty cheap:O(nlog n), since each element ofa is processed throughmerge once for each array it gets put in, and the recursion only goesO(log n) layers deep before we hit 1-element arrays.
The reason that we can’t easily transform this into an iterative version is that themergeSort function is not tail-recursive: not only does it call itself twice, but it also needs to free the temporary arrays at the end. Because the algorithm has to do these tasks on the way back up the stack, we need to keep the stack around to track them.
One issue with a recursive functions is that it becomes harder to estimate its asymptotic complexity. Unlike loops, where we can estimate the cost by simply multiplying the number of iterations by the cost of each iteration, the cost of a recursive function depends on the cost of its recursive calls. This would make it seem that we would need to be able to compute the cost of the function before we could compute the cost of the function.
Fortunately, for most recursive functions, the size of the input drops whenever we recurse. So the cost can be expressed in terms of arecurrence, a formula for the costT(n) on an input of sizen in terms of the cost on smaller inputs. Some examples:
There arestandard tools for solving many of the recurrences that arise in common algorithms, but these are not usually needed for our purposes, since there are only a handful of recurrences that are likely to come up in practice and we already solved most of them. Here is a table of some of the more common possibilities:
| Recurrence | Solution | Example |
|---|---|---|
| T(n) = T(n − 1) + O(1) | T(n) = O(n) | Finding a maximum |
| T(n) = T(n − 1) + O(n) | T(n) = O(n2) | Selection sort |
| T(n) = T(n/2) + O(1) | T(n) = O(log n) | Binary search |
| T(n) = 2T(n/2) + O(n) | T(n) = O(nlog n) | Mergesort |
Linked lists are about the simplest data structure beyond arrays. They aren’t very efficient for many purposes, but have very good performance for certain specialized applications.
The basic idea is that instead of storingn items in one big array, we store each item in its ownstruct, and each of thesestructs includes a pointer to the nextstruct in the list (with a null pointer to indicate that there are no more elements). If we follow the pointers we can eventually reach all of the elements.
For example, if we declare the struct holding each element like this:
struct elt {struct elt *next;/* pointer to next element in the list */int contents;/* contents of this element */};We can build a structure like this:
The box on the far left is not astruct elt, but astruct elt *; in order to keep track of the list we need a pointer to the first element. As usual in C, we will have to do all the work of allocating these elements and assigning the right pointer values in the right places ourselves.
The selling point of linked lists in comparison to arrays is that inserting or removing elements can be cheap: at the front of the list, inserting a new element just requires allocating anotherstruct and hooking up a few pointers, while removing an element just requires moving the pointer to the first element to point to the second element instead, and then freeing the first element.
For example here’s what happens the linked list above looks like after we insert a new element at the front:
To make this work, we need to change two pointers: the head pointer and thenext pointer in the new element holding 0. These operations aren’t affected by the size of the rest of the list and so takeO(1) time.
Removal is the reverse of installation: We patch out the first element by shifting the head pointer to the second element, then deallocate it withfree. (We do have to be careful to get any data we need out of it before calling free). This is also anO(1) operation.
The fact that we can add and remove elements at the start of linked lists for cheap makes them particularly useful for implementing astack, an abstract data type that supports operationspush (insert a new element on the top of the stack) andpop (remove and return the element at the top of the stack. Here is an example of a simple linked-list implementation of a stack, together with some test code:
#include<stdio.h>#include<stdlib.h>#include<assert.h>struct elt {struct elt *next;int value;};/* * We could make a struct for this, * but it would have only one component, * so this is quicker. */typedefstruct elt *Stack;#define STACK_EMPTY (0)/* push a new value onto top of stack */voidstackPush(Stack *s,int value){struct elt *e; e = malloc(sizeof(struct elt)); assert(e); e->value = value; e->next = *s; *s = e;}intstackEmpty(const Stack *s){return (*s ==0);}intstackPop(Stack *s){int ret;struct elt *e; assert(!stackEmpty(s)); ret = (*s)->value;/* patch out first element */ e = *s; *s = e->next; free(e);return ret;}/* print contents of stack on a single line */voidstackPrint(const Stack *s){struct elt *e;for(e = *s; e !=0; e = e->next) { printf("%d ", e->value); } putchar('\n');}intmain(int argc,char **argv){int i; Stack s; s = STACK_EMPTY;for(i =0; i <5; i++) { printf("push %d\n", i); stackPush(&s, i); stackPrint(&s); }while(!stackEmpty(&s)) { printf("pop gets %d\n", stackPop(&s)); stackPrint(&s); }return0;}Unlike most of our abstract data types, we do not include astruct representing the linked list itself. This is because the only thing we need to keep track of a linked list is the head pointer, and it feels a little silly to have astruct with just one component. But we might choose to do this if we wanted to make the linked list implementation opaque or allow for including more information later.
When the elements of a stack are small, or when a maximum number of elements is known in advance, it often makes sense to build a stack from an array (with a variable storing the index of the top element) instead of a linked list. The reason is that pushes and pops only require updating the stack pointer instead of callingmalloc orfree to allocate space, and pre-allocating is almost always faster than allocating as needed. This is the strategy used to store the function call stack in almost all programs (the exception is in languages like Scheme, where the call stack is allocated on the heap because stack frames may outlive the function call that creates them).
Stacks are last-in-first-out (LIFO) data structures: when we pop, we get the last item we pushed. What if we want a first-in-first-out (FIFO) data structure? Such a data structure is called aqueue and can also be implemented by a linked list. The difference is that if we wantO(1) time for both theenqueue (push) anddequeue (pop) operations, we must keep around pointers to both ends of the linked list.
So now we get something that looks like this:
Enqueuing a new element typically requires (a) allocating a newstruct to hold it; (b) making the old tailstruct point at the newstruct; and (c) updating thetail pointer to also point to the newstruct. There is a minor complication when the stack is empty; in this case instead of updatingtail->next we must put a pointer to the newstruct inhead. Dequeuing an element involves updating the head pointer and freeing the removedstruct, exactly like a stack pop.
Here is the queue above after enqueuing a new element 6. The updated pointers are indicated by dotted lines:
Because we are only changing two pointers, each of which we can reach by following a constant number of pointers from the mainstruct, we can do this inO(1) time.
There is a slight complication when we enqueue the very first element, because we need to update the head pointer instead of the pointer in the previous tail (which doesn’t yet exist). This requires testing for an empty queue in the enqueue routine, which we’ll do in the sample code below.
Dequeuing is easier because it requires updating only one pointer:
If we adopt the convention that a null inhead means an empty queue, and use this property to check if the queue is empty when enqueuing, we don’t even have to clear outtail when we dequeue the last element.
Here is a simple implementation of a queue holdingints, together with some test code showing how its behavior differs from a stack:
#include<stdio.h>#include<stdlib.h>#include<assert.h>/* standard linked list element */struct elt {struct elt *next;int value;};struct queue {struct elt *head;/* dequeue this next */struct elt *tail;/* enqueue after this */};/* create a new empty queue */struct queue *queueCreate(void){struct queue *q; q = malloc(sizeof(struct queue)); q->head = q->tail =0;return q;}/* add a new value to back of queue */voidenq(struct queue *q,int value){struct elt *e; e = malloc(sizeof(struct elt)); assert(e); e->value = value;/* Because I will be the tail, nobody is behind me */ e->next =0;if(q->head ==0) {/* If the queue was empty, I become the head */ q->head = e; }else {/* Otherwise I get in line after the old tail */ q->tail->next = e; }/* I become the new tail */ q->tail = e;}intqueueEmpty(conststruct queue *q){return (q->head ==0);}/* remove and return value from front of queue */intdeq(struct queue *q){int ret;struct elt *e; assert(!queueEmpty(q)); ret = q->head->value;/* patch out first element */ e = q->head; q->head = e->next; free(e);return ret;}/* print contents of queue on a single line, head first */voidqueuePrint(struct queue *q){struct elt *e;for(e = q->head; e !=0; e = e->next) { printf("%d ", e->value); } putchar('\n');}/* free a queue and all of its elements */voidqueueDestroy(struct queue *q){while(!queueEmpty(q)) { deq(q); } free(q);}intmain(int argc,char **argv){int i;struct queue *q; q = queueCreate();for(i =0; i <5; i++) { printf("enq %d\n", i); enq(q, i); queuePrint(q); }while(!queueEmpty(q)) { printf("deq gets %d\n", deq(q)); queuePrint(q); } queueDestroy(q);return0;}It is a bit trickier to build a queue out of an array than to build a stack. The difference is that while a stack pointer can move up and down, leaving the base of the stack in the same place, a naive implementation of a queue would have head and tail pointers both marching ever onward across the array leaving nothing but empty cells in their wake. While it is possible to have the pointers wrap around to the beginning of the array when they hit the end, if the queue size is unbounded the tail pointer will eventually catch up to the head pointer. At this point (as in a stack that overflows), it is necessary to allocate more space and copy the old elements over. See the section onring buffers for an example of how to do this.
Looping over a linked list is not hard if you have access to thenext pointers. (For a more abstract way to do this seeiterators.)
Let’s imagine somebody gave us a pointer to the firststruct stack in a list; call this pointerfirst. Then we can write a loop like this that prints the contents of the stack:
voidstackPrint(struct stack *first){struct stack *elt;for(elt = first; elt !=0; elt = elt->next) { puts(elt->book); }}There’s not a whole lot to notice here except thatfor is perfectly happy to iterate over something that isn’t a range of integers. The running time is linear in the length of the list (O(n)).
What if we want to loop over a linked list backwards? Thenext pointers all go the wrong way, so we have to save a trail of breadcrumbs to get back. The safest way to do this is to reverse the original list into an auxiliary list:
voidstackPrintReversed(struct stack *first){struct stack *elt; Stack s2;/* uses imperative implementation */ s2 = stackCreate();for(elt = first; elt !=0; elt = elt->next) { s2 = stackPush(s2, elt->book); } stackPrint(s2); stackDestroy(s2);}Pushing all the elements from the first list ontos2 puts the first element on the bottom, so when we prints2 out, it’s in the reverse order of the original stack.
We can also write a recursive function that prints the elements backwards. This function effectively uses the function call stack in place of the extra stacks2 above.
voidstackPrintReversedRecursive(struct stack *first){if(first !=0) {/* print the rest of the stack */ stackPrintReversedRecursive(first->next);/* then print the first element */ puts(first->book); }}The code instackPrintReversedRecursive is shorter than the code instackPrintReversed, and it is likely to be faster since it doesn’t require allocating a second stack and copying all the elements. But it will only work for small stacks: because the function call stack is really a fixed-size array, if the input tostackPrintReversedRecursive is too big the recursion will go too deep cause astack overflow.
If we want to do this sort of thing a lot, we should build adoubly-linked list, with a pointer in each element both to the next element and the previous element instead of a singly-linked list (see below for more).
Suppose we want a data structure that represents a line of elements where we can push or pop elements at either end. Such a data structure is known as adeque (pronounced like “deck”), and can be implemented with all operations takingO(1) time by adoubly-linked list, where each element has a pointer to both its successor and its predecessor.
An ordinary singly-linked list is not good enough. The reason is that even if we keep a pointer to both ends as in a queue, when it comes time to pop an element off the tail, we have no pointer to its predecessor ready to hand; the best we can do is scan from the head until we get to an element whose successor is the tail, which takesO(n) time.
So instead we need a doubly-linked list, where each node points to both its successor and predecessor. The most straightforward way to build this is to make it circular, and use an extra node whose value isn’t used to represent the head of the list. The resulting data structure might look something like this:
Below is an implementation of this structure. We have separated the interface indeque.h from the implementation indeque.c. This will allow us to change the implementation if we decide we don’t like it, without affecting any other code in the system.
A nice feature of this data structure is that we don’t need to use null pointers to mark the ends of the deque. Instead, each end is marked by a pointer to the extra head element. For an empty deque, this just means that the head points to itself. The cost of this is that to detect an empty deque we have to test for equality with the head (which might be slightly more expensive that just testing for null) and the head may contain some wasted space for its missing value if we allocate it like any other element.20
To keep things symmetric, we implement the pointers as an array, indexed by the directionsDEQUE_FRONT andDEQUE_BACK (defined indeque.h). This means we can use the same code to push or pop on either end of the deque.
typedefstruct deque Deque;#define DEQUE_FRONT (0)#define DEQUE_BACK (1)#define DEQUE_EMPTY (-1)/* returned by dequePop if deque is empty *//* return a new empty deque */Deque *dequeCreate(void);/* push new value onto direction side of deque d */void dequePush(Deque *d,int direction,int value);/* pop and return first value on direction side of deque d *//* returns DEQUE_EMPTY if deque is empty */int dequePop(Deque *d,int direction);/* return 1 if deque contains no elements, 0 otherwise */int dequeIsEmpty(const Deque *d);/* free space used by a deque */void dequeDestroy(Deque *d);#include<stdlib.h>#include<assert.h>#include<stddef.h>/* for offsetof */#include"deque.h"#define NUM_DIRECTIONS (2)struct deque {struct deque *next[NUM_DIRECTIONS];int value;};Deque *dequeCreate(void){ Deque *d;/* * We don't allocate the full space for this object * because we don't use the value field in the dummy head. * * Saving these 4 bytes doesn't make a lot of sense here, * but it might be more significant if the value field was larger. */ d = malloc(offsetof(struct deque, value));/* test is to deal with malloc failure */if(d) { d->next[DEQUE_FRONT] = d->next[DEQUE_BACK] = d; }return d;}voiddequePush(Deque *d,int direction,int value){struct deque *e;/* new element */ assert(direction == DEQUE_FRONT || direction == DEQUE_BACK); e = malloc(sizeof(struct deque)); assert(e); e->next[direction] = d->next[direction]; e->next[!direction] = d; e->value = value; d->next[direction] = e; e->next[direction]->next[!direction] = e;/* preserves invariant */}intdequePop(Deque *d,int direction){struct deque *e;int retval; assert(direction == DEQUE_FRONT || direction == DEQUE_BACK); e = d->next[direction];if(e == d) {return DEQUE_EMPTY; }/* else remove it */ d->next[direction] = e->next[direction]; e->next[direction]->next[!direction] = d; retval = e->value; free(e);return retval;}intdequeIsEmpty(const Deque *d){return d->next[DEQUE_FRONT] == d;}voiddequeDestroy(Deque *d){while(!dequeIsEmpty(d)) { dequePop(d, DEQUE_FRONT); } free(d);}And here is some test code:
Thedeque.h file carefully avoids revealing any details of the implementation of a deque. This allows us to replace the implementation with a different implementation that is more efficient in its use of both time and space, at the cost of additional code complexity. Below is a replacement fordeque.c that uses aring buffer in place of the circular linked list.
The idea of a ring buffer is to store the deque elements in an array, with a pointer to the first element and a length field that says how many elements are in the deque. The information needed to manage the array (which is allocated usingmalloc) is stored in astruct.
The sequence of elements wraps around the endpoints of the array, leaving a gap somewhere in the middle. Deque pushes extend the sequence into this gap from one side or another, while pops increase the size of the gap. If the user wants to do a push and the array is full, we build a new, larger deque, move all the elements there, and then transplant all the bits of the newstruct deque into the old one. This transplant trick avoids changing the address of thestruct deque that the user needs to access it.
#include<stdlib.h>#include<assert.h>#include"deque.h"/* * Alternative implementation of a deque using a ring buffer. * * Conceptually, this is an array whose indices wrap around at * the endpoints. * * The region in use is specified by a base index pointing * to the first element, and a length count giving the number * of elements. A size field specifies the number of slots * in the block. * * Picture: * * --------------------------------------------------- * |7|8|9| | | | | | | | | | | | | | | | |1|2|3|4|5|6| * --------------------------------------------------- * ^ ^ * | | * base + length - 1 base * */struct deque {size_t base;/* location of front element */size_t length;/* length of region in use */size_t size;/* total number of positions in contents */int *contents;};#define INITIAL_SIZE (8)/* create a new deque of the given size */static Deque *dequeCreateInternal(size_t size){struct deque *d; d = malloc(sizeof(struct deque)); assert(d); d->base =0; d->length =0; d->size = size; d->contents = malloc(sizeof(int) * d->size); assert(d->contents);return d;}/* return a new empty deque */Deque *dequeCreate(void){return dequeCreateInternal(INITIAL_SIZE);}voiddequePush(Deque *d,int direction,int value){struct deque *d2;/* replacement deque if we grow */int *oldContents;/* old contents of d *//* * First make sure we have space. */if(d->length == d->size) {/* nope */ d2 = dequeCreateInternal(d->size *2);/* evacuate d */while(!dequeIsEmpty(d)) { dequePush(d2, DEQUE_BACK, dequePop(d, DEQUE_FRONT)); }/* do a transplant from d2 to d *//* but save old contents so we can free them */ oldContents = d->contents; *d = *d2;/* this is equivalent to copying the components one by one *//* these are the pieces we don't need any more */ free(oldContents); free(d2); }/* * This requires completely different code * depending on the direction, which is * annoying. */if(direction == DEQUE_FRONT) {/* d->base is unsigned, so we have to check for zero first */if(d->base ==0) { d->base = d->size -1; }else { d->base--; } d->length++; d->contents[d->base] = value; }else { d->contents[(d->base + d->length++) % d->size] = value; }}/* pop and return first value on direction side of deque d *//* returns DEQUE_EMPTY if deque is empty */intdequePop(Deque *d,int direction){int retval;if(dequeIsEmpty(d)) {return DEQUE_EMPTY; }/* else */if(direction == DEQUE_FRONT) {/* base goes up by one, length goes down by one */ retval = d->contents[d->base]; d->base = (d->base+1) % d->size; d->length--;return retval; }else {/* length goes down by one */return d->contents[(d->base + --d->length) % d->size]; }}intdequeIsEmpty(const Deque *d){return d->length ==0;}voiddequeDestroy(Deque *d){ free(d->contents); free(d);}Here is aMakefile that compilestestDeque.c against both the linked list and the ring buffer implementations. You can domake time to race them against each other.
CC=gccCFLAGS=-Wall -O3 -g3# how many iterations for testITERATIONS=10000000VALGRIND_ITERATIONS=100all: testDeque testRingBuffertest: all./testDeque$(ITERATIONS)valgrind -q --leak-check=yes ./testDeque$(VALGRIND_ITERATIONS)./testRingBuffer$(ITERATIONS)valgrind -q --leak-check=yes ./testRingBuffer$(VALGRIND_ITERATIONS)time: alltime ./testDeque$(ITERATIONS)time ./testRingBuffer$(ITERATIONS)testDeque: testDeque.o deque.o$(CC)$(CFLAGS) -o$@$^testRingBuffer: testDeque.o ringBuffer.o$(CC)$(CFLAGS) -o$@$^clean:$(RM) testDeque testRingBuffer *.oFor some applications, there is no obvious starting or ending point to a list, and a circular list (where the last element points back to the first) may be appropriate. Circular doubly-linked lists can also be used to build deques; a single pointer into the list tracks the head of the deque, with some convention adopted for whether the head is an actual element of the list (at the front, say, with its left neighbor at the back) or a extra element that is not considered to be part of the list.
The selling point of circular doubly-linked lists as a concrete data structure is that insertions and deletions can be done anywhere in the list with only local information. For example, here are some routines for manipulating a doubly-linked list directly. We’ll make our lives easy and assume (for the moment) that the list has no actual contents to keep track of.
#include<stdlib.h>/* directions for doubly-linked list next pointers */#define RIGHT (0)#define LEFT (1)struct elt {struct elt *next[2];};typedefstruct elt *Elt;/* create a new circular doubly-linked list with 1 element *//* returns 0 on allocation error */EltlistCreate(void){ Elt e; e = malloc(sizeof(*e));if(e) { e->next[LEFT] = e->next[RIGHT] = e; }return e;}/* remove an element from a list *//* Make sure you keep a pointer to some other element! *//* does not free the removed element */voidlistRemove(Elt e){/* splice e out */ e->next[RIGHT]->next[LEFT] = e->next[LEFT]; e->next[LEFT]->next[RIGHT] = e->next[RIGHT];}/* insert an element e into list in direction dir from head */voidlistInsert(Elt head,int dir, Elt e){/* fill in e's new neighbors */ e->next[dir] = head->next[dir]; e->next[!dir] = head;/* make neigbhors point back at e */ e->next[dir]->next[!dir] = e; e->next[!dir]->next[dir] = e;}/* split a list, removing all elements between e1 and e2 *//* e1 is the leftmost node of the removed subsequence, e2 rightmost *//* the removed elements are formed into their own linked list *//* comment: listRemove could be implemented as listSplit(e,e) */voidlistSplit(Elt e1, Elt e2){/* splice out the new list */ e2->next[RIGHT]->next[LEFT] = e1->next[LEFT]; e1->next[LEFT]->next[RIGHT] = e2->next[RIGHT];/* fix up the ends */ e2->next[RIGHT] = e1; e1->next[LEFT] = e2;}/* splice a list starting at e2 after e1 *//* e2 becomes e1's right neighbor *//* e2's left neighbor becomes left neighbor of e1's old right neighbor */voidlistSplice(Elt e1, Elt e2){/* fix up tail end */ e2->next[LEFT]->next[RIGHT] = e1->next[RIGHT]; e1->next[RIGHT]->next[LEFT] = e2->next[LEFT];/* fix up e1 and e2 */ e1->next[RIGHT] = e2; e2->next[LEFT] = e1;}/* free all elements of the list containing e */voidlistDestroy(Elt e){ Elt target; Elt next;/* we'll free elements until we get back to e, then free e *//* note use of pointer address comparison to detect end of loop */for(target = e->next[RIGHT]; target != e; target = next) { next = target->next[RIGHT]; free(target); } free(e);}The above code might or might not actually work. What if it doesn’t? It may make sense to include some consistency testing code that we can run to see if our pointers are all going to the right place:
/* assert many things about correctness of the list *//* Amazingly, this is guaranteed to abort or return no matter how badly screwed up the list is. */voidlistConsistencyTest(Elt e){ Elt check; assert(e !=0); check = e;do {/* are our pointers consistent with our neighbors? */ assert(check->next[RIGHT]->next[LEFT] == check); assert(check->next[LEFT]->next[RIGHT] == check);/* on to the next */ check = check->next[RIGHT]; }while(check != e);}What if we want to store something in this list? The simplest approach is to extend the definition ofstruct elt:
But then we can only use the code for one particular type of data. An alternative approach is to define a newElt-plus struct:
and then use pointer casts to convert the fancy structs intoElts:
The trick here is that as long as the initial part of thestruct fancyElt looks like astruct elt, any code that expects astruct elt will happily work with it and ignore the fields that happen to be sitting later in memory. (This is howC++ inheritance works.)
The downside is that if something needs to be done with the other fields (e.g freeinge->name ife is freed), then theElt functions won’t know to do this. So if you use this trick you should be careful.
A similar technique usingvoid * pointers can be used to implementgeneric containers.
Linked lists are good for any task that involves inserting or deleting elements next to an element you already have a pointer to; such operations can usually be done inO(1) time. They generally beat arrays (even resizeable arrays) if you need to insert or delete in the middle of a list, since an array has to copy any elements above the insertion point to make room; if inserts or deletes always happen at the end, an array may be better.
Linked lists are not good for any operation that requires random access, since reaching an arbitrary element of a linked list takes as much asO(n) time. For such applications, arrays are better if you don’t need to insert in the middle; if you do, you should use some sort oftree.
Linked lists are often the primary data structure infunctional programming languages. The reason of this is that the functional programming style tries to avoid modifying data, which makes arrays expensive since changing one value might mean creating a new copy of the entire array. With linked lists, it may be possible for some operations to create a new list that shares most of its structure with the old list; an example would be pushing or popping the top of a stack. This kind of sharing is awkward in languages like C because it means that a struct implementing part of a list might have multiple pointers to it, making it tricky to detect when it is safe to free the struct.
A description of many different kinds of linked lists with pictures can be found inthe WikiPedia article on the subject.
Animated versions can be found athttp://www.cs.usfca.edu/~galles/visualization/Algorithms.html.
One of the hard parts about computer programming is that, in general,programs are bigger than brains. Unless you have an unusally capacious brain, it is unlikely that you will be able to understand even a modestly large program in its entirety. So in order to be able to write and debug large programs, it is important to be able to break it up into pieces, where each piece can be treated as a tool whose use and description is simpler (and therefor fits in your brain better) than its actual code. Then you can forget about what is happening inside that piece, and just concentrate on how to use it from the outside.
This process of wrapping functionality up in a box and forgetting about its internals is calledabstraction, and it is the single most important concept in computer science. In these notes we will describe a particular kind of abstraction, the construction ofabstract data types or ADTs. Abstract data types are data types whose implementation is not visible to their user; from the outside, all the user knows about an ADT is what operations can be performed on it and what those operations are supposed to do.
ADTs have an outside and an inside. The outside is called theinterface; it consists of the minimal set of type and function declarations needed to use the ADT. The inside is called theimplementation; it consists of type and function definitions, and sometime auxiliary data or helper functions, that arenot visible to users of the ADT. This separation between interface and implementation is called theabstraction barrier, and allows the implementation to change without affecting the rest of the program.
What joins the implementation to the interface is anabstraction function. This is a function (in the mathematical sense) that takes any state of the implementation and trims off any irrelevant details to leave behind an idealized pictures of what the data type is doing. For example, a linked list implementation translates to a sequence abstract data type by forgetting about the pointers used to hook up the elements and just keeping the sequence of elements themselves. To exclude bad states of the implementation (for example, a singly-linked list that loops back on itself instead of having a terminating null pointer), we may have arepresentation invariant, which is just some property of the implementation that is always true. Representation invariants are also useful for detecting when we’ve bungled our implementation, and a good debugging strategy for misbehaving abstract data type implementations is often to look for the first point at which they violated some property that we thought was an invariant.
Some programming language include very strong mechanisms for enforcing abstraction barriers. C relies somewhat more on politeness, and as a programmer you violate an abstraction barrier (by using details of an implementation that are supposed to be hidden) at your peril. In C, the interface will typically consist of function and type declarations contained in a header file, with implementation made up of the corresponding function definitions (and possibly a few extrastatic functions) in one or more.c files. Theopaque struct technique can be used to hide implementation details of the type.
Object-oriented languages like Java make a distinction betweenboxed types, which are allocated on the heap and accessed through pointers, andunboxed types orprimitive types, which are small enough to be passed around directly.
In C terms, a boxed type would typically be implemented as astruct allocated usingmalloc, often by aconstructor function that takes responsibility for doing the allocation, initializing the underlyingstruct as appropriate, and returning a pointer to the newstruct. This gives aninstance of the type that lives until we decide to free it. A separatedestructor function will need to be provided to free any space used by thestruct; this is especially important if the data structure requires more than one call tomalloc, although in simple cases it may be enough for the destructor to just be a simple wrapper aroundfree. When passing an instance of a boxed type to or from a function, we copy a pointer to the heap-allocated structure. This structure is not itself copied, which is more efficient than copying if the structure is large, and which allows the function to modify the structure if needed without having to explicitly return an updated version. Typically the functions only expected to modify the underlying structure will be themutators provided by the abstract data type interface, and other functions that wish to operate on the structure will use these mutators to apply their changes. Similarly, the interface will often provideaccessor functions to allow other functions the ability to obtain information about an instance without breaking the abstraction barrier that hides the actual implementation.
An unboxed type doesn’t use the heap, and an instance of an unboxed type is typically not a pointer but an actual C type that implements an instance directly. The built-in types likeint anddouble are examples of unboxed types, but we can also construct unboxed types on top ofstructs. This works best when thestructs are small enough that copying them around is a cheap operation and where the intended interpretation of an instance is as a constant value. An example might be anRGBA color value packed into a 32-bit struct:
This has the same size as anint on a typical machine, so allocating in on the heap and passing around a pointer to it would add a lot of unnecessary space overhead. Depending on how much abstraction we want to enforce, we might still expect the user to access values of this type only through functions provided by a library. For example, we might expect that the user only creates new RGBA colors through functions we provide, as in:
struct rgba c;c = RBGAfromValues(122,11,13,255);// goodc = RGBAfromName("orange");// goodc.red =12;// please don't do thisUnfortunately, we can’t apply theopaque struct trick to unboxedstructs, so there is no mechanism in the C language to exclude the bad case above. This makes unboxed types preferable if we want a (somewhat) enforceable abstraction barrier.
Even without the issue of abstraction barriers, most of the data structures we will build will be large enough and complex enough that it makes sense to implement them as boxed types just to avoid copying costs. But there may be occasions where the overhead of a boxed type is too much, so using an unboxed type makes more sense.
It is technically possible to have an unboxed type that includes pointers to instances of boxed types. This is almost always a mistake, as it may lead to multiple instances of the unboxed type all pointing to a single instance of the boxed type. An example would be if we defined a sized byte string as follows:
struct bytes {size_t size;// size of datachar *data;// malloc'd block };// bad approachstruct bytes b; b = bytesFromString("hi there");// returns struct bytesstruct bytes b2; b2 = b;// add some more text to the end bytesAppendString(&b2,", world!");This is legal C, but now we start with at least twostruct bytes instances floating around that point to the same data block. So we will have to be vary careful to decide which one to call a destructor on, as calling a destructor on one of the instances will invalidate all the others. The issue is further confused if we have mutators (likebytesAppendString) that can replace thedata field or change thesize field in an instance: what happens tob when we callbytesAppendString onb2?
A safer approach is to make ourbytes type boxed:
struct bytes {size_t size;// size of datachar *data;// malloc'd block };// better approachstruct bytes *b; b = bytesFromString("hi there");// returns struct bytes *struct bytes *b2; b2 = b;// add some more text to the end bytesAppendString(b2,", world!");Now there is just a single instance ofstruct bytes that we happen to have two pointers to. We will still need to be careful about invalidating one of these pointers when we call a destructor on the other, but callingbytesAppendString will have the expected effect on the single underlying object.
Too much abstraction at once can be hard to take, so let’s look at a concrete example of an abstract data type. This ADT will represent an infinite sequence ofints. Each instance of theSequence type supports a single operationseq_next that returns the nextint in the sequence. We will also need to provide one or more constructor functions to generate newSequences, and a destructorfunction to tear them down.
Here is an example of a typical use of aSequence:
voidseq_print(Sequence s,int limit){int i;for(i = seq_next(s); i < limit; i = seq_next(s)) { printf("%d\n", i); }}Note thatseq_print doesn’t need to know anything at all about what aSequence is or howseq_next works in order to print out all the values in the sequence until it hits one greater than or equal to limit. This is a good thing— it means that we can use with any implementation ofSequence we like, and we don’t have to change it ifSequence orseq_next changes.
In C, the interface of an abstract data type will usually be declared in a header file, which is included both in the file that implements the ADT (so that the compiler can check that the declarations match up with the actual definitions in the implementation. Here’s a header file for sequences:
/* opaque struct: hides actual components of struct sequence, * which are defined in sequence.c */typedefstruct sequence *Sequence;/* constructors *//* all our constructors return a null pointer on allocation failure *//* returns a Sequence representing init, init+1, init+2, ... */Sequence seq_create(int init);/* returns a Sequence representing init, init+step, init+2*step, ... */Sequence seq_create_step(int init,int step);/* destructor *//* destroys a Sequence, recovering all interally-allocated data */void seq_destroy(Sequence);/* accessor *//* returns the first element in a sequence not previously returned */int seq_next(Sequence);Here we have defined two different constructors forSequences, one of which gives slightly more control over the sequence than the other. If we were willing to put more work into the implementation, we could imagine building a very complicatedSequence type that supported a much wider variety of sequences (for example, sequences generated by functions or sequences read from files); but we’ll try to keep things simple for now. We can always add more functionality later, since the users won’t notice if theSequence type changes internally.
The implementation of an ADT in C is typically contained in one (or sometimes more than one).c file. This file can be compiled and linked into any program that needs to use the ADT. Here is our implementation ofSequence:
#include<stdlib.h>#include"sequence.h"struct sequence {int next;/* next value to return */int step;/* how much to increment next by */};Sequenceseq_create(int init){return seq_create_step(init,1);}Sequenceseq_create_step(int init,int step){ Sequence s; s = malloc(sizeof(*s));if(s ==0)return0; s->next = init; s->step = step;return s;}voidseq_destroy(Sequence s){ free(s);}intseq_next(Sequence s){int ret;/* saves the old value before we increment it */ ret = s->next; s->next += s->step;return ret;}Things to note here: the definition ofstruct sequence appears only in this file; this means that only the functions defined here can (easily) access thenext andstep components. This protectsSequences to a limited extent from outside interference, and defends against users who might try to “violate the abstraction boundary” by examining the components of aSequence directly. It also means that if we change the components or meaning of the components instruct sequence, we only have to fix the functions defined insequence.c.
Now that we havesequence.h andsequence.c, how do we use them? Let’s suppose we have a simple main program:
#include<stdio.h>#include"sequence.h"voidseq_print(Sequence s,int limit){int i;for(i = seq_next(s); i < limit; i = seq_next(s)) { printf("%d\n", i); }}intmain(int argc,char **argv){ Sequence s; Sequence s2; puts("Stepping by 1:"); s = seq_create(0); seq_print(s,5); seq_destroy(s); puts("Now stepping by 3:"); s2 = seq_create_step(1,3); seq_print(s2,20); seq_destroy(s2);return0;}We can compilemain.c andsequence.c together into a single binary with the commandgcc main.c sequence.c. Or we can build aMakefile which will compile the two files separately and then link them. Usingmake may be more efficient, especially for large programs consisting of many components, since if we make any changesmake will only recompile those files we have changed. So here is ourMakefile:
CC=c99CFLAGS=-g3 -Wallall: seqprinterseqprinter: main.o sequence.o$(CC)$(CFLAGS) -o$@$^test: seqprinter./seqprinter# these rules say to rebuild main.o and sequence.o if sequence.h changesmain.o: main.c sequence.hsequence.o: sequence.c sequence.hclean:$(RM) -f seqprinter *.oAnd now runningmake test produces this output. Notice how the built-inmake variables$@ and$^ expand out to the left-hand side and right-hand side of the dependency line for buildingseqprinter.
$ make testgcc -g3 -Wall -c -o main.o main.cgcc -g3 -Wall -c -o sequence.o sequence.cgcc -g3 -Wall -o seqprinter main.o sequence.o./seqprinterStepping by 1:01234Now stepping by 3:14710131619Now we’ve seen how to implement an abstract data type. How do we choose when to use when, and what operations to give it? Let’s try answering the second question first.
Parnas’s Principle is a statement of the fundamental idea ofinformation hiding, which says that abstraction boundaries should be as narrow as possible:
(David Parnas, “On the Criteria to Be Used in Decomposing Systems into Modules,”Communications of the ACM, 15(12): 1059–1062, 1972.)
For ADTs, this means we should provide as few functions for accessing and modifying the ADT as we can get away with. TheSequence type we defined early has a particularly narrow interface; the developer ofSequence (whoever is writingsequence.c) needs to know nothing about what its user wants except for the arguments passed in toseq_create orseq_create_step, and the user only needs to be able to callseq_next. More complicated ADTs might provide larger sets of operations, but in general we know that an ADT provides a successful abstraction when the operations are all “natural” ones given our high-level description. If we find ourselves writing a lot of extra operations to let users tinker with the guts of our implementation, that may be a sign that either we aren’t taking our abstraction barrier seriously enough, or that we need to put the abstraction barrier in a different place.
The short answer: Whenever you can.
A better answer: The best heuristic I know for deciding what ADTs to include in a program is to write down a description of how your program is going to work. For each noun or noun phrase in the description, either identify a built-in data type to implement it or design an abstract data type.
For example: a grade database maintains a list of students, and for each student it keeps a list of grades. So here we might want data types to represent:
If grades are simple, we might be able to make them just beints (or maybedoubles); to be on the safe side, we should probably create aGrade type with atypedef. The other types are likely to be more complicated. Each student might have in addition to their grades a long list of other attributes, such as a name, an email address, etc. By wrapping students up as abstract data types we can extend these attributes if we need to, or allow for very general implementations (say, by allowing a student to have an arbitrary list of keyword-attribute pairs). The two kinds of lists are likely to be examples ofsequence types; we’ll be seeing a lot of ways to implement these as the course progresses. If we want to perform the same kinds of operations on both lists, we might want to try to implement them as a single list data type, which then is specialized to hold either students or grades; this is not always easy to do in C, but we’ll see examples of how to do this, too.
Whether or not this set of four types is the set we will finally use, writing it down gives us a place to start writing our program. We can start writing interface files for each of the data types, and then evolve their implementations and the main program in parallel, adjusting the interfaces as we find that we have provided too little (or too much) data for each component to do what it must.
Ahash table is a randomized data structure that supports the INSERT, DELETE, and FIND operations in expectedO(1) time. The core idea behind hash tables is to use ahash function that maps a large keyspace to a smaller domain of array indices, and then use constant-time array operations to store and retrieve the data.
A hash table is typically used to implement adictionary data type, where keys are mapped to values, but unlike an array, the keys are not conveniently arranged as integers0, 1, 2, …. Dictionary data types are a fundamental data structure often found inscripting languages likeAWK,Perl,Python,PHP,Lua, orRuby. For example, here is some Python code that demonstrates use of a dictionary accessed using an array-like syntax:
title= {}# empty dictionarytitle["Barack"]="President"user="Barack"print("Welcome "+ title[user]+" "+ user)In C, we don’t have the convenience of reusing[] for dictionary lookups (we’d needC++ for that), but we can still get the same effect with more typing using functions. For example, using an abstract dictionary in C might look like this:
Dict *title;constchar *user;title = dictCreate();dictSet(title,"Barack","President");user ="Barack";printf("Welcome %s %s\n", dictGet(title, user), user);As with other abstract data types, the idea is that the user of the dictionary type doesn’t need to know how it is implemented. For example, we could implement the dictionary as an array ofstructs that we search through, but that would be expensive:O(n) time to find a key in the worst case.
Closely related to a dictionary is aset, which has keys but no values. It’s usually pretty straightforward to turn an implementation of a dictionary into a set (leave out the values) or vice versa (add values to the end of keys but don’t use them in searching).
If our keys were conveniently named0, 1, 2, …, n − 1, we could simply use an array, and be able to find a record given a key in constant time. Unfortunately, naming conventions for most objects are not so convenient, and even enumerations like Social Security numbers are likely to span a larger range than we want to allocate. But we would like to get the constant-time performance of an array anyway.
The solution is to feed the keys through some hash functionH, which maps them down to array indices. So in a database of people, to find “Smith, Wayland”, we would first computeH(“Smith, Wayland”) = 137 (say), and then look in position 137 in the array. Because we are always using the same functionH, we will always be directed to the same position 137.
But what ifH(“Smith, Wayland”) andH(“Hephaestos”) both equal 137? Now we have acollision, and we have to resolve it by finding some way to either (a) effectively store both records in a single array location, or (b) move one of the records to a new location that we can still find later. Let’s consider these two approaches separately.
We can’t really store more than one record in an array location, but we can fake it by making each array location be a pointer to a linked list. Every time we insert a new element in a particular location, we simply add it to this list.
Since the cost of scanning a linked list is linear in its size, this means that the worst-case cost of searching for a particular key will be linear in the number of keys in the table that hash to the same location. Under the assumption that the hash function is a random function (which does not mean that it returns random values every time you call it but instead means that we picked one of the many possible hash functions uniformly at random), on average we getn/m elements in each list.
So on average a failed search takesO(n/m) time.
This quantityn/m is called theload factor of the hash table and is often written asα. If we want our hash table to be efficient, we will need to keep this load factor down. If we can guarantee that it’s a constant, then we get constant-time searches.
Withopen addressing, we store only one element per location, and handle collisions by storing the extra elements in other unused locations in the array. To find these other locations, we fix someprobe sequence that tells us where to look ifA[H(x)] contains an element that is notx. A typical probe sequence (calledlinear probing) is justH(x), H(x) + 1, H(x) + 2, …, wrapping around at the end of the array. The idea is that if we can’t put an element in a particular place, we just keep walking up through the array until we find an empty slot. As long as we follow the same probe sequence when looking for an element, we will be able to find the element again. If we are looking for an element and reach an empty location, then we know that the element is not present in the table.
For open addressing, we always have thatα = n/m is less than or equal to 1, since we can’t store more elements in the table than we have locations. In fact, we must ensure that the load factor is strictly less than 1, or some searches will never terminate because they never reach an empty location. Assumingα < 1 and that the hash function is uniform, it is possible to calculate the worst-case expected cost of a FIND operation, which as before will occur when we have an unsuccessful FIND. Though we won’t do this calculation here, the result is bounded by1/(1 − n/m), which gets pretty bad ifn/m is very close to1, but is a constant as long asn/m is bounded by a constant (say3/4, which makes the expected number of probes at most4).
There are many popular hash functions out there. The strongest, such as theSHA-2 family, are believed to provide strong cryptographic guarantees such as making it very difficult to find an input that hashes to a particular output value. For building hash tables, these cryptographic hash function are usually excessive. For this we want simpler functions that faster and easier to code.
The essential idea of any hash function is that we want to produce as output a sequence of bits of some fixed length that (a) depends on the entire input, and (b) looks random. The first part means that changing any bit of the input should substantially change the output; the second means that the hash function should in some weak sense appear to be unpredictable. At the same time we would like the function to be something we can compute quickly. Below we will some simple hash functions that seem to work well in practice.
These are probably good enough for anything you will do in this class. If you find yourself writing high-performance code based on hashing, and are not just using some standard library that already does this, some examples of more sophisticated hash functions in widespread use areSipHash, which uses a secret initialization to make it harder to attack, andxxHash, which gets very high speed by computing several hashes in parallel on different parts of the input stream and combining the results, exploiting parallelism inside the CPU.
We want our hash function to look as close as it can to a random function, but random functions are (provably) expensive to store. So in practice we do something simpler and hope for the best. If the keys are large integers, a typical approach is to just compute the remainder modm. This can cause problems ifm is, say, a power of 2, since it may be that the low-order bits of all the keys are similar, which will produce lots of collisions. So in practice with this methodm is typically chosen to be a large prime.
What if we want to hash strings instead of integers? The trick is to treat the strings as integers. Given a stringa1a2a3…ak, we represent it as∑iaibi, whereb is a base chosen to be larger than the number of characters. We can then feed this resulting huge integer to our hash function. Typically we do not actually compute the huge integer directly, but instead compute its remainder modm, as in this short C function:
/* treat strings as base-256 integers *//* with digits in the range 1 to 255 */#define BASE (256)size_thash(constchar *s,size_t m){size_t h;unsignedconstchar *us;/* cast s to unsigned const char * *//* this ensures that elements of s will be treated as having values >= 0 */ us = (unsignedconstchar *) s; h =0;while(*us != '\0') { h = (h * BASE + *us) % m; us++; }return h;}The division method works best whenm is a prime, as otherwise regularities in the keys can produce clustering in the hash values. (Consider, for example, what happens ifm is 256). But this can be awkward for computing hash functions quickly, because computing remainders is a relatively slow operation. However, even if we don’t use the division method explicitly, we often use it implicitly, if only to crunch a large hash value down to the size of our actual table.
To avoid the cost of division, most commonly-used hash functions replace the modulusm with some power of two2k, and replace the base with a prime, relying on the multiplier to break up patterns in the input. This yields themultiplication method, of which there are many versions. One of the best is theFowler/Noll/Vo hash function, which maintains a fixed-size integer hash value, and for each byte of the input, XORs in the byte then scrambles the result by multiplying by a carefully chosen prime number. The resulting function is called FNV-1a, and it looks like this:
#include<stdint.h>#include<stdlib.h>#define FNV_PRIME_64 ((1ULL<<40)+(1<<8)+0xb3)#define FNV_OFFSET_BASIS_64 (14695981039346656037ULL)uint64_tFNV1a(size_t n,constunsignedchar *bytes){uint64_t h = FNV_OFFSET_BASIS_64;for(size_t i =0; i < n; i++) { h ^= bytes[i]; h *= FNV_PRIME_64; }return h;}There is a distinct FNV prime for different word sizes, each of which is chosen based on number-theoretic magic. The initial starting value oroffset basis is chosen arbitrarily but should not be zero.
FNV-1a has been observed to work well for common data sets. However, because it’s fixed, there is no guarantee that an adversary can’t supply a set ofn keys that all hash to the same location, giving very bad performance. We can avoid this problem by using a hash function that is generated independently of the keys, as described below.
Universal families of hash functions choose the hash function randomly, from some set of possible functions that is small enough that we can write our random choice down. By using a random choice, we make it hard for an adversary who doesn’t know our choice to force collisions. But by fixing the random choice, we can still use the same hash function for every lookup.
The property that makes a family of hash functions{Hr} universal is that, for any distinct keysx andy, the probability thatr is chosen so thatHr(x) = Hr(y) is exactly1/m.
Why is this important? Recall that for chaining, the expected number of collisions between an elementx and other elements was just the sum over all particular elementsy of the probability thatx collides with that particular element. IfHr is drawn from a universal family, this probability is1/m for eachy, and we get the samen/m expected collisions as ifHr were completely random.
Several universal families of hash functions are known. Here is a simple one, known as tabulation hashing or Zobrist hashing, that works when the size of the keyspace and the size of the output space are both powers of2. Let the keyspace consist ofn-bit strings and letm = 2k. Then the random indexr consists ofnk independent random bits organized asnm-bit stringsa1a2…an. To compute the hash function of a particular inputx, compute the bitwise exclusive or ofai for each positioni where thei-th bit ofx is1.
We can implement this in C as
/* implements universal hashing using random bit-vectors in x *//* assumes number of elements in x is at least BITS_PER_CHAR * MAX_STRING_SIZE */#define BITS_PER_CHAR (8)/* not true on all machines! */#define MAX_STRING_SIZE (128)/* we'll stop hashing after this many */#define MAX_BITS (BITS_PER_CHAR * MAX_STRING_SIZE)size_thash(constchar *s,size_t x[]){size_t h;unsignedconstchar *us;int i;unsignedchar c;int shift;/* cast s to unsigned const char * *//* this ensures that elements of s will be treated as having values >= 0 */ us = (unsignedconstchar *) s; h =0;for(i =0; *us !=0 && i < MAX_BITS; us++) { c = *us;for(shift =0; shift < BITS_PER_CHAR; shift++, i++) {/* is low bit of c set? */if(c &0x1) { h ^= x[i]; }/* shift c to get new bit in lowest position */ c >>=1; } }return h;}As you can see, this requires a lot of bit-fiddling. It also fails if we get a lot of strings that are identical for the firstMAX_STRING_SIZE characters. Conceivably, the latter problem could be dealt with by growingx dynamically as needed. But we also haven’t addressed the question of where we get these random values from—see the section onrandomization for some possibilities.
In practice, universal families of hash functions are seldom used, since a reasonable fixed hash function is unlikely to be correlated with any patterns in the actual input. But they are useful for demonstrating provably good performance.
All of the running time results for hash tables depend on keeping the load factorα small. But as more elements are inserted into a fixed-size table, the load factor grows without bound. The usual solution to this problem is rehashing: when the load factor crosses some threshold, we create a new hash table of size2n or thereabouts and migrate all the elements to it.
This approach raises the worst-case cost of an insertion toO(n). However, we can bring theexpected cost down toO(1) by rehashing only with probabilityO(1/n) for each insert after the threshold is crossed. Or we can applyamortized analysis to argue that the amortized cost (total cost divided by number of operations) isO(1) assuming we double the table size on each rehash. Neither the expected-cost nor the amortized-cost approaches actually change the worst-case cost, but they make it look better by demonstrating that we at least don’t incur that cost every time.
With enough machinery, it may be possible todeamortize the cost of rehashing by doing a little bit of it with every insertion. The idea is to build the new hash table incrementally, and start moving elements to it once it is fully initialized. This requires keeping around two copies of the hash table and searching both, and for most purposes is more trouble than it’s worth. But a mechanism like this is often used for real-time garbage collection, where it’s important not to have the garbage collector lock up the entire system while it does its work.
Here is a very low-overhead hash table based on open addressing. The application is rapidly verifying ID numbers in the range 000000000 to 999999999 by checking them against a list of known good IDs. Since the quantity of valid ID numbers may be very large, a goal of the mechanism is to keep the amount of extra storage used as small as possible. This implementation uses a tunable overhead parameter. Setting the parameter to a high value makes lookups fast but requires more space per ID number in the list. Setting it to a low value can reduce the storage cost arbitrarily close to 4 bytes per ID, at the cost of increasing search times.
Here is the header file giving the interface:
typedefstruct idList *IDList;#define MIN_ID (0)#define MAX_ID (999999999)/* build an IDList out of an unsorted array of n good ids *//* returns 0 on allocation failure */IDList IDListCreate(int n,int unsortedIdList[]);/* destroy an IDList */void IDListDestroy(IDList list);/* check an id against the list *//* returns nonzero if id is in the list */int IDListContains(IDList list,int id);And here is the implementation:
#include<stdlib.h>#include<assert.h>#include"idList.h"/* overhead parameter that determines both space and search costs *//* must be strictly greater than 1 */#define OVERHEAD (1.1)#define NULL_ID (-1)struct idList {int size;int ids[1];/* we'll actually malloc more space than this */};IDListIDListCreate(int n,int unsortedIdList[]){ IDList list;int size;int i;int probe; size = (int) (n * OVERHEAD +1); list = malloc(sizeof(*list) +sizeof(int) * (size-1));if(list ==0)return0;/* else */ list->size = size;/* clear the hash table */for(i =0; i < size; i++) { list->ids[i] = NULL_ID; }/* load it up */for(i =0; i < n; i++) { assert(unsortedIdList[i] >= MIN_ID); assert(unsortedIdList[i] <= MAX_ID);/* hashing with open addressing by division *//* this MUST be the same pattern as in IDListContains */for(probe = unsortedIdList[i] % list->size; list->ids[probe] != NULL_ID; probe = (probe +1) % list->size); assert(list->ids[probe] == NULL_ID); list->ids[probe] = unsortedIdList[i]; }return list;}voidIDListDestroy(IDList list){ free(list);}intIDListContains(IDList list,int id){int probe;/* this MUST be the same pattern as in IDListCreate */for(probe = id % size; list->ids[probe] != NULL_ID; probe = (probe +1) % size) {if(list->ids[probe] == id) {return1; } }return0;}Here is a more complicated string to string dictionary based on chaining.
typedefstruct dict *Dict;/* create a new empty dictionary */Dict DictCreate(void);/* destroy a dictionary */void DictDestroy(Dict);/* insert a new key-value pair into an existing dictionary */void DictInsert(Dict,constchar *key,constchar *value);/* return the most recently inserted value associated with a key *//* or 0 if no matching key is present */constchar *DictSearch(Dict,constchar *key);/* delete the most recently inserted record with the given key *//* if there is no such record, has no effect */void DictDelete(Dict,constchar *key);#include<stdint.h>#include<stdlib.h>#include<assert.h>#include<string.h>#include"dict.h"struct elt {struct elt *next;char *key;char *value;};struct dict {int size;/* size of the pointer table */int n;/* number of elements stored */struct elt **table;};#define INITIAL_SIZE (1024)#define GROWTH_FACTOR (2)#define MAX_LOAD_FACTOR (1)/* dictionary initialization code used in both DictCreate and grow */DictinternalDictCreate(int size){ Dict d;int i; d = malloc(sizeof(*d)); assert(d !=0); d->size = size; d->n =0; d->table = malloc(sizeof(struct elt *) * d->size); assert(d->table !=0);for(i =0; i < d->size; i++) d->table[i] =0;return d;}DictDictCreate(void){return internalDictCreate(INITIAL_SIZE);}voidDictDestroy(Dict d){int i;struct elt *e;struct elt *next;for(i =0; i < d->size; i++) {for(e = d->table[i]; e !=0; e = next) { next = e->next; free(e->key); free(e->value); free(e); } } free(d->table); free(d);}#define FNV_PRIME_64 ((1ULL<<40)+(1<<8)+0xb3)#define FNV_OFFSET_BASIS_64 (14695981039346656037ULL)uint64_thash_function(constchar *bytes){uint64_t h = FNV_OFFSET_BASIS_64;for(size_t i =0; bytes[i] != '\0'; i++) { h = h ^ bytes[i]; h *= FNV_PRIME_64; }return h;}staticvoidgrow(Dict d){ Dict d2;/* new dictionary we'll create */struct dict swap;/* temporary structure for brain transplant */int i;struct elt *e; d2 = internalDictCreate(d->size * GROWTH_FACTOR);for(i =0; i < d->size; i++) {for(e = d->table[i]; e !=0; e = e->next) {/* note: this recopies everything *//* a more efficient implementation would * patch out the strdups inside DictInsert * to avoid this problem */ DictInsert(d2, e->key, e->value); } }/* the hideous part *//* We'll swap the guts of d and d2 *//* then call DictDestroy on d2 */ swap = *d; *d = *d2; *d2 = swap; DictDestroy(d2);}/* insert a new key-value pair into an existing dictionary */voidDictInsert(Dict d,constchar *key,constchar *value){struct elt *e;unsignedlong h; assert(key); assert(value); e = malloc(sizeof(*e)); assert(e); e->key = strdup(key); e->value = strdup(value); h = hash_function(key) % d->size; e->next = d->table[h]; d->table[h] = e; d->n++;/* grow table if there is not enough room */if(d->n >= d->size * MAX_LOAD_FACTOR) { grow(d); }}/* return the most recently inserted value associated with a key *//* or 0 if no matching key is present */constchar *DictSearch(Dict d,constchar *key){struct elt *e;for(e = d->table[hash_function(key) % d->size]; e !=0; e = e->next) {if(!strcmp(e->key, key)) {/* got it */return e->value; } }return0;}/* delete the most recently inserted record with the given key *//* if there is no such record, has no effect */voidDictDelete(Dict d,constchar *key){struct elt **prev;/* what to change when elt is deleted */struct elt *e;/* what to delete */for(prev = &(d->table[hash_function(key) % d->size]); *prev !=0; prev = &((*prev)->next)) {if(!strcmp((*prev)->key, key)) {/* got it */ e = *prev; *prev = e->next; free(e->key); free(e->value); free(e);return; } }}And here is some (very minimal) test code.
#include<stdio.h>#include<assert.h>#include"dict.h"intmain(){ Dict d;char buf[512];int i; d = DictCreate(); DictInsert(d,"foo","hello world"); puts(DictSearch(d,"foo")); DictInsert(d,"foo","hello world2"); puts(DictSearch(d,"foo")); DictDelete(d,"foo"); puts(DictSearch(d,"foo")); DictDelete(d,"foo"); assert(DictSearch(d,"foo") ==0); DictDelete(d,"foo");for(i =0; i <10000; i++) { sprintf(buf,"%d", i); DictInsert(d, buf, buf); } DictDestroy(d);return0;}The first rule of programming is that you should never write the same code twice. Suppose that you happen to have lying around a dictionary type whose keys areints and whose values are strings. Tomorrow you realize that what you really want is a dictionary type whose keys are strings and whose values areints, or one whose keys areints but whose values are stacks. If you haven different types that may appear as keys or values, can you avoid writingn2 different dictionary implementations to get every possible combination?
Many languages provide special mechanisms to supportgeneric types, where part of the type is not specified. It’s as if you could declare an array in C to be an array of some type to be determined later, and then write functions that operate on any such array without knowing what the missing type is going to be (templates in C++ are an example of such a mechanism). Unfortunately, C does not provide generic types. But by aggressive use of function pointers andvoid *, it is possible to fake them.
Below is an example of an interface to a generic dictionary type for storing maps from constant values to constant values. Thevoid * pointers are used to avoid having to declare exactly what kinds of keys and values the dictionary will contain.
/* Set dict[key] = value. *//* Both key and value are copied internally. *//* If data is the null pointer, remove dict[key]. */void dictSet(Dict d,constvoid *key,constvoid *value);/* Return dict[key], or null if dict[key] has not been set. */constvoid *dictGet(Dict d,constvoid *key);We’ll also need a constructor and destructor, but we’ll get to those in a moment. First we need to think about whatdictSet anddictGet are supposed to do, and how we might possibly be able to implement them. Suppose we want to build a dictionary with strings as both keys and values. Internally, this might be represented as some sort of hash table or tree. Suppose it’s a hash table. Now, given somevoid *key, we’d like to be able to compute its hash value. But we don’t know what typekey points to, and if we guess wrong we are likely to end up with segmentation faults or worse. So we need some way to register a hash function for our keys, whatever type they might really be behind thatvoid *.
Similarly, we will want to be able to compare keys for equality (since not all keys that hash together will necessarily be the same), and we may want to be able to copy keys and values so that the data inside the dictionary is not modified if somebody changes a value passed in from the outside. So we need a fair bit of information about keys and values. We’ll organize all of this information in a struct made up of function pointers. (This includes a few extra components that came up while writing the implementation.)
/* Provides operations for working with keys or values */struct dictContentsOperations {/* hash function */unsignedlong (*hash)(constvoid *datum,void *arg);/* returns nonzero if *datum1 == *datum2 */int (*equal)(constvoid *datum1,constvoid *datum2,void *arg);/* make a copy of datum that will survive changes to original */void *(*copy)(constvoid *datum,void *arg);/* free a copy */void (*delete)(void *datum,void *arg);/* extra argument, to allow further specialization */void *arg;};We could write a similar but smaller struct for values, but to save a little bit of effort in the short run we’ll use the samestruct for both keys and values. We can now write a constructor for our generic dictionary that consumes two such structs that provide operations for working on keys and values, respectively:
/* create a new dictionary with given key and value operations *//* Note: valueOps.hash and valueOps.equal are not used. */Dict dictCreate(struct dictContentsOperations keyOps,struct dictContentsOperations valueOps);So now to create a dict, we just need to fill in twodictContentsOperations structures. For convenience, it might be nice ifdict.c provided some preloaded structures for common types likeints and strings. We can also use thearg field instruct dictContentsOperations to make the keys and values themselves be parameterized types, for example a type of byte-vectors of given length.
We can declare these various convenience structures indict.h as
/* Some predefined dictContentsOperations structures *//* * DictIntOps supports int's that have been cast to (void *), e.g.: * d = dictCreate(DictIntOps, DictIntOps); * dictSet(d, (void *) 1, (void * 2)); * x = (int) dictGet(d, (void * 1)); */struct dictContentsOperations DictIntOps;/* * Supports null-terminated strings, e.g.: * d = dictCreate(DictStringOps, DictStringOps); * dictSet(d, "foo", "bar"); * s = dictGet(d, "foo"); * Note: no casts are needed since C automatically converts * between (void *) and other pointer types. */struct dictContentsOperations DictStringOps;/* * Supports fixed-size blocks of memory, e.g.: * int x = 1; * int y = 2; * d = dictCreate(dictMemOps(sizeof(int)), dictMemOps(sizeof(int)); * dictSet(d, &x, &y); * printf("%d", *dictGet(d, &x); */struct dictContentsOperations dictMemOps(int size);We’ll define the operations inDictIntOps to expectints cast directly tovoid *, the operations inDictStringOps to expectchar * cast tovoid *, and the operations indictMemOps(size) will expectvoid * arguments pointing to blocks of the given size. There is a subtle difference between a dictionary usingDictIntOps anddictMemOps(sizeof(int)); in the former case, keys and values are theints themselves (after being case), which in the latter, keys and values are pointers toints.
Implementations of these structures can be foundbelow.
To make a dictionary that maps strings to ints, we just call:
and then we can do things like:
If we find ourselves working with an integer-valued dictionary a lot, we might want to define a few macros or inline functions to avoid having to type casts all the time.
To implement our generic dictionary, we just take our favorite non-generic hash table, and replace any calls to fixed hash functions, copier,free, etc. with calls to elements of the appropriate structure. The result is shown below.
typedefstruct dict *Dict;/* Provides operations for working with keys or values */struct dictContentsOperations {/* hash function */unsignedlong (*hash)(constvoid *datum,void *arg);/* returns nonzero if *datum1 == *datum2 */int (*equal)(constvoid *datum1,constvoid *datum2,void *arg);/* make a copy of datum that will survive changes to original */void *(*copy)(constvoid *datum,void *arg);/* free a copy */void (*delete)(void *datum,void *arg);/* extra argument, to allow further specialization */void *arg;};/* create a new dictionary with given key and value operations *//* Note: valueOps.hash and valueOps.equal are not used. */Dict dictCreate(struct dictContentsOperations keyOps,struct dictContentsOperations valueOps);/* free a dictionary and all the space it contains *//* This will call the appropriate delete function for all keys and *//* values. */void dictDestroy(Dict d);/* Set dict[key] = value. *//* Both key and value are copied internally. *//* If data is the null pointer, remove dict[key]. */void dictSet(Dict d,constvoid *key,constvoid *value);/* Return dict[key], or null if dict[key] has not been set. */constvoid *dictGet(Dict d,constvoid *key);/* Some predefined dictContentsOperations structures *//* * DictIntOps supports int's that have been cast to (void *), e.g.: * d = dictCreate(DictIntOps, DictIntOps); * dictSet(d, (void *) 1, (void * 2)); * x = (int) dictGet(d, (void * 1)); */struct dictContentsOperations DictIntOps;/* * Supports null-terminated strings, e.g.: * d = dictCreate(DictStringOps, DictStringOps); * dictSet(d, "foo", "bar"); * s = dictGet(d, "foo"); * Note: no casts are needed since C automatically converts * between (void *) and other pointer types. */struct dictContentsOperations DictStringOps;/* * Supports fixed-size blocks of memory, e.g.: * int x = 1; * int y = 2; * d = dictCreate(dictMemOps(sizeof(int)), dictMemOps(sizeof(int)); * dictSet(d, &x, &y); * printf("%d", *dictGet(d, &x); */struct dictContentsOperations dictMemOps(int size);#include<stdlib.h>#include<string.h>#include"dict.h"struct dictElt {unsignedlong hash;/* full hash of key */void *key;void *value;struct dictElt *next;};struct dict {int tableSize;/* number of slots in table */int numElements;/* number of elements */struct dictElt **table;/* linked list heads *//* these save arguments passed at creation */struct dictContentsOperations keyOps;struct dictContentsOperations valueOps;};#define INITIAL_TABLESIZE (16)#define TABLESIZE_MULTIPLIER (2)#define TABLE_GROW_DENSITY (1)DictdictCreate(struct dictContentsOperations keyOps,struct dictContentsOperations valueOps){ Dict d;int i; d = malloc(sizeof(*d));if(d ==0)return0; d->tableSize = INITIAL_TABLESIZE; d->numElements =0; d->keyOps = keyOps; d->valueOps = valueOps; d->table = malloc(sizeof(*(d->table)) * d->tableSize);if(d->table ==0) { free(d);return0; }for(i =0; i < d->tableSize; i++) d->table[i] =0;return d;}voiddictDestroy(Dict d){int i;struct dictElt *e;struct dictElt *next;for(i =0; i < d->tableSize; i++) {for(e = d->table[i]; e !=0; e = next) { next = e->next; d->keyOps.delete(e->key, d->keyOps.arg); d->valueOps.delete(e->value, d->valueOps.arg); free(e); } } free(d->table); free(d);}/* return pointer to element with given key, if any */staticstruct dictElt *dictFetch(Dict d,constvoid *key){unsignedlong h;int i;struct dictElt *e; h = d->keyOps.hash(key, d->keyOps.arg); i = h % d->tableSize;for(e = d->table[i]; e !=0; e = e->next) {if(e->hash == h && d->keyOps.equal(key, e->key, d->keyOps.arg)) {/* found it */return e; } }/* didn't find it */return0;}/* increase the size of the dictionary, rehashing all table elements */staticvoiddictGrow(Dict d){struct dictElt **old_table;int old_size;int i;struct dictElt *e;struct dictElt *next;int new_pos;/* save old table */ old_table = d->table; old_size = d->tableSize;/* make new table */ d->tableSize *= TABLESIZE_MULTIPLIER; d->table = malloc(sizeof(*(d->table)) * d->tableSize);if(d->table ==0) {/* put the old one back */ d->table = old_table; d->tableSize = old_size;return; }/* else *//* clear new table */for(i =0; i < d->tableSize; i++) d->table[i] =0;/* move all elements of old table to new table */for(i =0; i < old_size; i++) {for(e = old_table[i]; e !=0; e = next) { next = e->next;/* find the position in the new table */ new_pos = e->hash % d->tableSize; e->next = d->table[new_pos]; d->table[new_pos] = e; } }/* don't need this any more */ free(old_table);}voiddictSet(Dict d,constvoid *key,constvoid *value){int tablePosition;struct dictElt *e; e = dictFetch(d, key);if(e !=0) {/* change existing setting */ d->valueOps.delete(e->value, d->valueOps.arg); e->value = value ? d->valueOps.copy(value, d->valueOps.arg) :0; }else {/* create new element */ e = malloc(sizeof(*e));if(e ==0) abort(); e->hash = d->keyOps.hash(key, d->keyOps.arg); e->key = d->keyOps.copy(key, d->keyOps.arg); e->value = value ? d->valueOps.copy(value, d->valueOps.arg) :0;/* link it in */ tablePosition = e->hash % d->tableSize; e->next = d->table[tablePosition]; d->table[tablePosition] = e; d->numElements++;if(d->numElements > d->tableSize * TABLE_GROW_DENSITY) {/* grow and rehash */ dictGrow(d); } }}constvoid *dictGet(Dict d,constvoid *key){struct dictElt *e; e = dictFetch(d, key);if(e !=0) {return e->value; }else {return0; }}/* int functions *//* We assume that int can be cast to void * and back without damage */staticunsignedlong dictIntHash(constvoid *x,void *arg) {return (int) x; }staticint dictIntEqual(constvoid *x,constvoid *y,void *arg){return ((int) x) == ((int) y);}staticvoid *dictIntCopy(constvoid *x,void *arg) {return (void *) x; }staticvoid dictIntDelete(void *x,void *arg) { ; }struct dictContentsOperations DictIntOps = { dictIntHash, dictIntEqual, dictIntCopy, dictIntDelete,0};/* common utilities for string and mem */staticunsignedlong hashMem(constunsignedchar *s,int len){unsignedlong h;int i; h =0;for(i =0; i < len; i++) { h = (h <<13) + (h >>7) + h + s[i]; }return h;}staticvoid dictDeleteFree(void *x,void *arg) { free(x); }/* string functions */staticunsignedlong dictStringHash(constvoid *x,void *arg){return hashMem(x, strlen(x));}staticint dictStringEqual(constvoid *x,constvoid *y,void *arg){return !strcmp((constchar *) x, (constchar *) y);}staticvoid *dictStringCopy(constvoid *x,void *arg){constchar *s;char *s2; s = x; s2 = malloc(sizeof(*s2) * (strlen(s)+1)); strcpy(s2, s);return s2;}struct dictContentsOperations DictStringOps = { dictStringHash, dictStringEqual, dictStringCopy, dictDeleteFree,0};/* mem functions */staticunsignedlong dictMemHash(constvoid *x,void *arg){return hashMem(x, (int) arg);}staticint dictMemEqual(constvoid *x,constvoid *y,void *arg){return !memcmp(x, y, (size_t) arg);}staticvoid *dictMemCopy(constvoid *x,void *arg){void *x2; x2 = malloc((size_t) arg); memcpy(x2, x, (size_t) arg);return x2;}struct dictContentsOperationsdictMemOps(int len){struct dictContentsOperations memOps; memOps.hash = dictMemHash; memOps.equal = dictMemEqual; memOps.copy = dictMemCopy; memOps.delete = dictDeleteFree; memOps.arg = (void *) len;return memOps;}And here is some test code and a Makefile:test-dict.c,tester.h,tester.c,Makefile.
Generic containers as described in the previous section improve on containers for fixed types, but they are still limited to storing a single type. In some circumstances it makes sense to design data structures or functions to work on any type that supplies the right operations. To make this work, we will attach the functions for manipulating an object to the object itself. This is the central idea behindobject-oriented programming.
As with most sophisticated programming techniques, C doesn’t provide any direct support for object-oriented programming, but it is possible to make it work anyway by taking advantage of C’s flexibility. We will use two basic ideas: first, we’ll give each object a pointer to amethod table in the form of a struct full of function pointers, and second we’ll take advantage of the fact that C lays out struct fields in the order we declare them to allowsubtyping, where some objects are represented by structs that extend the base struct used for all objects. (We could apply a similar technique to the method tables to allow subtypes to add more methods, but for the example in this section we will keep things simple and provide the same methods for all objects.)
Here is the file that declares the base object type. Note that we expose the details of bothstruct object andstruct methods, since subtypes will need to implement these.
#ifndef _OBJECT_H#define _OBJECT_H// truncated version of an object// real object will have more fields after methods// we expose this for implementersstruct object {conststruct methods *methods;};typedefstruct object Object;struct methods { Object *(*clone)(const Object *self);void (*print)(const Object *self);void (*destroy)(Object *self);};#endifObjects in this system have three methods:clone, which makes a copy of the object,print, which sends a representation of the object tostdout, anddestroy, which frees the object. Each of these methods takes the object itself as its first argumentself, since C provides no other mechanism to tell these functions which object to work on. If we needed to pass additional arguments to a method, we would add these afterself, but for these simple methods this is not needed.
To implement a subtype of object, we extendstruct object by defining a new struct type that hasmethods as its first field, but may have additional fields to store the state of the object. We also write functions implementing the new type’s methods, and build a constant global struct containing pointers to these functions, which all objects of the new type will point to. Finally, we build a constructor function that allocates and initializes an instance of the new object. This will be the only function that is exported from the module for our subtype, because anything else we want to do to an object, we can do by calling methods. This gives a very narrow interface to each subtype module, which is good, because the narrower the interface, the less likely we run into collisions with other modules.
Here is the very short header file for a subtype ofObject that holdsints. Most of this is just the usual header file boilerplate.
And here is the actual implementation. Note that it is only in this file that functions can access the representationstruct intObject of these objects. Everywhere else, they just look likeObjects. This does come with a cost: each of the method implementations has to cast in and out ofObject pointers to work with the underlyingstruct intObjects, and even though this corresponds to precisely zero instructions at run time, if we fail to be disciplined enough to only applyintObject methods tointObjects, the compiler will not catch the error for us.
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include"intObject.h"// wrap ints up as objects// this extends Object with extra fieldstruct intObject {struct methods *methods;int value;};staticvoid printInt(const Object *s);static Object *cloneInt(const Object *s);staticvoid destroyInt(Object *s);staticstruct methods intObjectMethods = { cloneInt, printInt, destroyInt};staticvoidprintInt(const Object *self){ printf("%d", ((struct intObject *) self)->value);}static Object *cloneInt(const Object *self){return intObjectCreate(((struct intObject *) self)->value);}staticvoiddestroyInt(Object *self){// we don't have any pointers, so we can just free the block free(self);}Object *intObjectCreate(int value){struct intObject *self = malloc(sizeof(struct intObject)); assert(self); self->methods = &intObjectMethods; self->value = value;return (Object *) self;}Having implemented these objects, we can use them in any context where the three provided methods are enough to work with them. For example, here is the interface to a stack that works on arbitrarilyObjects.
#ifndef _STACK_H#define _STACK_H#include"object.h"// basic stack implementation// stack is a pointer to its first element// caller will keep a pointer to thistypedefstruct elt *Stack;// create and destroy stacksStack *stackCreate(void);void stackDestroy(Stack *);// usual functionsvoid stackPush(Stack *s, Object *);// don't call this on an empty stackObject *stackPop(Stack *s);// returns true if not emptyint stackNotEmpty(const Stack *s);// print the elements of a stack to stdout// using function printvoid stackPrint(const Stack *s);#endifInternally, this stack will use theclone method to ensure that it gets its own copy of anything pushed onto the stack instackPush, to protect against the caller later destroying or modifying the object being pushed; theprint method to print objects instackPrint; and thedestroy method to clean up instackDestroy. The implementation looks like this:
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include"stack.h"struct elt {struct elt *next; Object *value;};// create and destroy stacksStack *stackCreate(void) { Stack *s; s = malloc(sizeof(Stack)); assert(s); *s =0;// empty stackreturn s;}voidstackDestroy(Stack *s) { Object *o;while(stackNotEmpty(s)) { o = stackPop(s); o->methods->destroy(o); } free(s);}// usual functionsvoidstackPush(Stack *s, Object *value) {struct elt *e = malloc(sizeof(struct elt)); e->next = *s; e->value = value->methods->clone(value); *s = e;}// don't call this on an empty stackObject *stackPop(Stack *s) { assert(stackNotEmpty(s));struct elt *e = *s; Object *ret = e->value; *s = e->next; free(e);return ret;}// returns true if not emptyintstackNotEmpty(const Stack *s) {return *s !=0;}// print the elements of a stack to stdoutvoidstackPrint(const Stack *s) {for(struct elt *e = *s; e; e = e->next) { e->value->methods->print(e->value); putchar(' '); } putchar('\n');}Because we are working in C, method calls are a little verbose, since we have to follow the method table pointer and supply the self argument ourself. Object-oriented programming languages generally provide syntactic sugar to simplify this task (and avoid possible errors). So a messy line in C like
e->value->methods->print(e->value);would look in C++ like
e->value.print();Something similar would happen in other object-oriented languages like Python or Java.
Becausestack.c accesses objects only through their methods, it will work on any objects, even objects of different types mixed together. Below is a program that mixesint objects as defined above with string objects defined instring.h andstring.c:
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<string.h>#include"object.h"#include"intObject.h"#include"stringObject.h"#include"stack.h"#define N (3)// do some stack stuffintmain(int argc,char **argv){char str[] ="hi"; Object *o;int n = N;if(argc >=2) { n = atoi(argv[1]); } Stack *s = stackCreate();for(int i =0; i < n; i++) {// push a string onto the stack str[0] ='a' + i; o = stringObjectCreate(str); stackPush(s, o); o->methods->destroy(o); stackPrint(s);// push an int onto the stack o = intObjectCreate(i); stackPush(s, o); o->methods->destroy(o); stackPrint(s); }while(stackNotEmpty(s)) { o = stackPop(s); putchar('['); o->methods->print(o); o->methods->destroy(o); fputs("] ", stdout); stackPrint(s); } stackDestroy(s);return0;}This pushes an alternating pile of ints and strings onto the stack, printing the stack after each push, then pops and prints these objects, again printing the stack after each push. Except for having to choose betweenintObjectCreate andstringObjectCreate at creation time, nothing intestStack.c depends on which of these two subtypes each object is.
Of course, to buildtestStack we need to link together a lot of files, which we can do with thisMakefile. Runningmake test gives the following output, demonstrating that we are in fact successfully mixing ints with strings:
gcc -std=c99 -Wall -g3 -c -o testStack.o testStack.cgcc -std=c99 -Wall -g3 -c -o stack.o stack.cgcc -std=c99 -Wall -g3 -c -o intObject.o intObject.cgcc -std=c99 -Wall -g3 -c -o stringObject.o stringObject.cgcc -std=c99 -Wall -g3 -o testStack testStack.o stack.o intObject.o stringObject.ofor i in ; do ./$i; donefor i in testStack; do valgrind -q --leak-check=full ./$i; doneai 0 ai bi 0 ai 1 bi 0 ai ci 1 bi 0 ai 2 ci 1 bi 0 ai [2] ci 1 bi 0 ai [ci] 1 bi 0 ai [1] bi 0 ai [bi] 0 ai [0] ai [ai]As with generic containers, the nice thing about this approach is that if we want to add more subtypes of object, we can do so the same way we did withintObject andstringObject, without having to ask anybody’s permission to change any of the code inobject.h,stack.h,stack.c, ortestStack.c. This is very different from what would happen, for example, if anObject was implemented as a tagged union, where adding a new type would require rewriting the code forObject. The cost is that we have to follow function pointers and be disciplined in how we use them.
Divide and conquer yields algorithms whose execution has a tree structure. Sometimes we build data structures that are also trees. It is probably not surprising that divide and conquer is the natural way to build algorithms that use such trees as inputs.
Here is a typical binary tree. It is binary because every node has at most two children; not all trees will have this property, but it is common for tree data structures that implement divide-and-conquer, because it yields simpler code.
This particular tree is alsocomplete because the nodes consist only ofinternal nodes with exactly two children andleaves with no children. Not all binary trees will be complete.
0 / \ 1 2 / \ 3 4 / \ 5 6 / \ 7 8Structurally, a complete binary tree consists of either a single node (a leaf) or a root node with a left and rightsubtree, each of which is itself either a leaf or a root node with two subtrees. The set of all nodes underneath a particular node x is called the subtree rooted at x.
Thesize of a tree is the number of nodes; a leaf by itself has size 1. Theheight of a tree is the length of the longest path; 0 for a leaf, at least one in any larger tree. Thedepth of a node is the length of the path from the root to that node. Theheight of a node is the height of the subtree of which it is the root, i.e. the length of the longest path from that node to some leaf below it. A nodeu is anancestor of a nodev ifv is contained in the subtree rooted atu; we may write equivalently thatv is adescendant ofu. Note that every node is both an ancestor and a descendant of itself. If we wish to exclude the node itself, we refer to aproper ancestor orproper descendant.
Trees arise in programs in two ways:
We’ll start by talking about general trees and then concentrate mostly on binary trees.
Data from the outside world is often organized in trees. This is particularly true for code of various kinds.
For example, HTML explicitly encodes a tree, where each node in the tree is either a formatting tag or raw text. Formatting tags translate into internal nodes, and the contents between the opening and closing tag translate to the children of these nodes. For example, the short HTML file
<HTML><HEAD><TITLE>Title!</TITLE></HEAD><BODY><P>This paragraph has<EM>emphasized text.</EM></P><P>This paragraph has<B>bold.</B></P></BODY>turns into the tree
To represent a tree like this in C, we need a recursive data structure that includes enough information to identify the type and contents of each node, as well as pointers to all the children of each internal node. Something like this might work:
struct htmlNode {enum { TYPE_TAG, TYPE_TEXT } type;char *value;// tag name or text contentsstruct htmlNode **kids;// if non-null, null-terminated sequence of pointers};Similarparse tree data structures are often used in programming language compilers and interpreters to represent source code. This allows the compiler/interpreter to concentrate on the structure of the code instead of having to worry about unstructured sequences of characters. A simple example is given in
Because of the complexity of handling arbitrary numbers of children, if we aren’t forced to do it, we will often instead use a binary tree, where each node has at most two children.
A binary tree typically looks like a linked list with an extra outgoing pointer from each element, as in
An alternative is to put the pointers to the children in an array. This lets us loop over both children, pass in which child we are interested in to a function as an argument, or even change the number of children:
Which approach we take is going to be a function of how much we like writingleft andright vschild[0] andchild[1]. A possible advantage ofleft andright is that it is harder to make mistakes that are not caught by the compiler (child[2]). Using the preprocessor, it is in principle possible to have your cake and eat it too (#define left child[0]), but I would not recommend this unless you are deliberately trying to confuse people.
Missing children (and the empty tree) are represented by null pointers. Typically, individual tree nodes are allocated separately usingmalloc; however, for high-performance use it is not unusual for tree libraries to do their own storage allocation out of large blocks obtained frommalloc.
Optionally, thestruct may be extended to include additional information such as a pointer to the node’s parent, hints forbalancing, or aggregate information about the subtree rooted at the node such as its size or the sum/max/average of the keys of its nodes.
When it is not important to be able to move large subtrees around simply by adjusting pointers, a tree may be represented implicitly by packing it into an array. This is a standard approach for implementingheaps, which we will see soon.
Pretty much everydivide and conquer algorithm for binary trees looks like this:
voiddoSomethingToAllNodes(struct node *root){if(root) { doSomethingTo(root); doSomethingToAllNodes(root->left); doSomethingToAllNodes(root->right); }}The function processes all nodes in what is called apreorder traversal, where the “preorder” part means that the root of any tree is processed first. Moving the call todoSomethingTo in between or after the two recursive calls yields aninorder orpostorder traversal, respectively.
In practice we usually want to extract some information from the tree. For example, this function computes the size of a tree:
inttreeSize(struct node *root){if(root ==0) {return0; }else {return1 + treeSize(root->left) + treeSize(root->right); }}and this function computes the height:
inttreeHeight(struct node *root){int lh;/* height of left subtree */int rh;/* height of right subtree */if(root ==0) {return -1; }else { lh = treeHeight(root->left); rh = treeHeight(root->right);return1 + (lh > rh ? lh : rh); }}Since both of these algorithms have the same structure, they both have the same asymptotic running time. We can compute this running time by observing that each recursive call totreeSize ortreeHeight that does not get a null pointer passed to it gets a different node (so there aren such calls), and each call that does get a null pointer passed to it is called by a routine that doesn’t, and that there are at most two such calls per node. Since the body of each call itself costsO(1) (no loops), this gives a total cost ofΘ(n).
So these are allΘ(n) algorithms.
For some binary trees we don’t store anything interesting in the internal nodes, using them only to provide paths to the leaves. We might reasonably ask if an algorithm that runs inO(n) time wheren is the total number of nodes still runs inO(m) time, wherem counts only the leaves. Forcomplete binary trees, we can show that we get the same asymptotic performance whether we count leaves only, internal nodes only, or both leaves and internal nodes.
LetT(n) be the number of internal nodes in a complete binary tree withn leaves. It is easy to see thatT(1) = 0 andT(2) = 1, but for larger trees there are multiple structures and so it makes sense to write a recurrence:T(n) = 1 + T(k) + T(n − k).
We can show by induction that the solution to this recurrence is exactlyT(n) = n − 1. We already have the base caseT(1) = 0. For largern, we haveT(n) = 1 + T(k) + T(n − k) = 1 + (k − 1) + (n − k − 1) = n − 1.
So a complete binary tree withΘ(n) nodes hasΘ(n) internal nodes andΘ(n) leaves; if we don’t care about constant factors, we won’t care which number we use.
So far we haven’t specified where particular nodes are placed in the binary tree. Most applications of binary trees put some constraints on how nodes relate to one another. Some possibilities:
Aheap is abinary tree in which each element has a key (or sometimespriority) that is less than the keys of its children. Heaps are used to implement thepriority queueabstract data type, which we’ll talk about first.
In a standard queue, elements leave the queue in the same order as they arrive. In a priority queue, elements leave the queue in order of decreasing priority: the DEQUEUE operation becomes a DELETE-MIN operation (or DELETE-MAX, if higher numbers mean higher priority), which removes and returns the highest-priority element of the priority queue, regardless of when it was inserted. Priority queues are often used in operating system schedulers to determine which job to run next: a high-priority job (e.g., turn on the fire suppression system) runs before a low-priority job (floss the cat) even if the low-priority job has been waiting longer.
Implementing a priority queue using an array or linked list is likely to be expensive. If the array or list is unsorted, it takesO(n) time to find the minimum element; if it is sorted, it takesO(n) time (in the worst case) to add a new element. So such implementations are only useful when the numbers of INSERT and DELETE-MIN operations are very different. For example, if DELETE-MIN is called only rarely but INSERT is called often, it may actually be cheapest to implement a priority queue as an unsorted linked list withO(1) INSERTs andO(n) DELETE-MINs. But if we expect that every element that is inserted is eventually removed, we want something for which both INSERT and DELETE-MIN are cheap operations.
A heap is a binary tree in which each node has a smaller key than its children; this property is called theheap property orheap invariant.
To insert a node in the heap, we add it as a new leaf, which may violate the heap property if the new node has a lower key than its parent. But we can restore the heap property (at least between this node and its parent) by swapping either the new node or its sibling with the parent, where in either case we move up the node with the smaller key. This may still leave a violation of the heap property one level up in the tree, but by continuing to swap small nodes with their parents we eventually reach the top and have a heap again. The time to complete this operation is proportional to the depth of the heap, which will typically beO(log n) (we will see how to enforce this in a moment).
To implement DELETE-MIN, we can easily find the value to return at the top of the heap. Unfortunately, removing it leaves a vacuum that must be filled in by some other element. The easiest way to do this is to grab a leaf (which probably has a very high key), and then float it down to where it belongs by swapping it with its smaller child at each iteration. After time proportional to the depth (againO(log n) if we are doing things right), the heap invariant is restored.
Similar local swapping can be used to restore the heap invariant if the priority of some element in the middle changes; we will not discuss this in detail.
It is possible to build a heap usingstructs and pointers, where each element points to its parent and children. In practice, heaps are instead stored in arrays, with an implicit pointer structure determined by array indices. For zero-based arrays as in C, the rule is that a node at positioni has children at positions2*i+1 and2*i+2; in the other direction, a node at positioni has a parent at position(i-1)/2 (which rounds down in C). This is equivalent to storing a heap in an array by reading through the tree inbreadth-first search order:
0 / \ 1 2/ \ / \3 4 5 6becomes
0 1 2 3 4 5 6This approach works best if there are no gaps in the array. So to maximize efficiency we make this “no gaps” policy part of the invariant. We can do so because we don’t care which leaf gets added when we do an INSERT, so we can make it be whichever leaf is at the end of the array. Similarly, in a DELETE-MIN operation, we can promote the element at the end of the array to the root before floating it down. Both these operations change the number of elements in the array, and INSERTs in particular might force us to reallocate eventually. So in the worst case INSERT can be an expensive operation, although as with growing hash tables, the amortized cost may still be small.
If we are presented with an unsorted array, we can turn it into a heap more quickly than theO(nlog n) time required to don INSERTs. The trick is to build the heap from the bottom up. This means starting with positionn − 1 and working back to position0, so that when it comes time to fix the heap invariant at positioni, we have already fixed it at all later positions (this is a form ofdynamic programming). Unfortunately, it is not quite enough simply to swapa[i] with its smaller child when we get there, because we could find thata[0] (say) was the largest element in the heap. But the cost of floatinga[i] down to its proper place will be proportional to its own height rather than the height of the entire heap. Since most of the elements of the heap are close to the bottom, the total cost will turn out to beO(n).
Bottom-up heapification is used in the Heapsort algorithm, which first does bottom-up heapification inO(n) time and then repeatedly calls DELETE-MAX to extract the largest remaining element. This is no faster than theO(nlog n) cost ofmergesort orquicksort in typical use, but requires very little auxiliary storage since we can maintain the heap in the bottom part of the same array whose top part stores the max elements extracted so far.
Here is a simple implementation of heapsort, that demonstrates how both bottom-up heapification and the DELETE-MAX procedure work by floating elements down to their proper places:
#include<stdio.h>#include<stdlib.h>#include<assert.h>/* max heap implementation *//* compute child 0 or 1 */#define Child(x, dir) (2*(x)+1+(dir))/* float value at position pos down */staticvoidfloatDown(int n,int *a,int pos){int x;/* save original value once */ x = a[pos];for(;;) {if(Child(pos,1) < n && a[Child(pos,1)] > a[Child(pos,0)]) {/* maybe swap with Child(pos, 1) */if(a[Child(pos,1)] > x) { a[pos] = a[Child(pos,1)]; pos = Child(pos,1); }else {/* x is bigger than both kids */break; } }elseif(Child(pos,0) < n && a[Child(pos,0)] > x) {/* swap with Child(pos, 0) */ a[pos] = a[Child(pos,0)]; pos = Child(pos,0); }else {/* done */break; } } a[pos] = x;}/* construct a heap bottom-up */staticvoidheapify(int n,int *a){int i;for(i = n-1; i >=0; i--) { floatDown(n, a, i); }}/* sort an array */voidheapSort(int n,int *a){int i;int tmp; heapify(n, a);for(i = n-1; i >0; i--) {/* swap max to a[i] */ tmp = a[0]; a[0] = a[i]; a[i] = tmp;/* float new a[0] down */ floatDown(i, a,0); }}#define N (100)#define MULTIPLIER (17)intmain(int argc,char **argv){int a[N];int i;if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; }for(i =0; i < N; i++) { a[i] = (i*MULTIPLIER) % N; }for(i =0; i < N; i++) { printf("%d ", a[i]); } putchar('\n'); heapSort(N, a);for(i =0; i < N; i++) { printf("%d ", a[i]); } putchar('\n');return0;}Abinary search tree is abinary tree in which each node has akey, and a node’s key must be greater than all keys in the subtree of its left-hand child and less than all keys in the subtree of its right-hand child. This allows a node to be searched for using essentially the same binary search algorithm used on sorted arrays.
/* returns pointer to node with given target key *//* or 0 if no such node exists */struct node *treeSearch(struct node *root,int target){if(root ==0 || root->key == target) {return root; }elseif(root->key > target) {return treeSearch(root->left, target); }else {return treeSearch(root->right, target); }}This procedure can be rewritten iteratively, which avoids stack overflow and is likely to be faster:
struct node *treeSearch(struct node *root,int target){while(root !=0 && root->key != target) {if(root->key > target) { root = root->left; }else { root = root->right; } }return root;}These procedures can be modified in the obvious way to deal with keys that aren’tints, as long as they can be compared (e.g., by usingstrcmp on strings).
As in ahash table, the insertion procedure mirrors the search procedure. We must be a little careful to avoid actually walking all the way down to a null pointer, since a null pointer now indicates a missing space for a leaf that we can fill with our new node. So the code is a little more complex.
// Insert a new key into a tree whose root is pointed to// by *parent, which should be 0 if tree is empty. May modify *parent.voidtreeInsert(struct tree **parent,int key){struct tree *newNode;for(;;) {if(*parent ==0) {// put it here *parent = malloc(sizeof(*newNode)); assert(*parent); (*parent)->key = key; (*parent)->left =0; (*parent)->right =0;return; }elseif(key == (*parent)->key) {// already presentreturn; }elseif(key < (*parent)->key) {// insert in left subtree parent = &(*parent)->left; }else {// insert in right subtree parent = &(*parent)->right; } }}Note that this function makes not attempt to keep the tree balanced. This may lead to very long paths if new keys are inserted in strictly increasing or strictly decreasing order.
Deletion is more complicated. If a node has no children, we can just remove it, and the rest of the tree stays the same. A node with one child can be spliced out, connecting its parent directly to its child. But with two children, we can’t do this.
The trick is to find the leftmost node in our target’s right subtree (or vice versa). This node exists assuming the target has two children. As in a hash table, we can then swap our target node with this more convenient node. Because it is the leftmost node, it has no left child, so we can delete it using the no-children or one-child case.
Searching for or inserting a node in a binary search tree withn nodes takes time proportional to the depth of the node. Inbalanced trees, where the nodes in each subtree are divided roughly evenly between the two child subtrees, this will beO(log n), but for a badly unbalanced tree, this might be as much asO(n). So making a binary search tree work efficiently requires keeping it balanced.
Anaugmented data structure stores additional information in each of its nodes that caches values that might otherwise be expensive to compute. For trees, this might include information like the size of a subtree (which can be useful forranking values, where we want to determine how many elements of the tree are smaller), the height of a subtree, or other summary information like the sum of all the keys in a subtree.
Augmented data structures, in a sense, violate the no-separate-but-equal rule that says we shouldn’t store the same information in different places. The reason we try to avoid this is that it’s trouble if the two copies diverge, and by not having two copies in the first place there is no possibility that they contradict each other. But in this case the reduced cost justifies breaking this rule.
The idea is that when we insert a new element into an augmented tree, it only changes the height/size/sum/etc. values for nodes on the path from the root to the new value. Since each of these aggregate values can be computed for a node inO(1) time from the values in its children, we can update all the aggregate values on our way back up the stack after doing the insertion at a cost ofO(1) per node. This will give a total cost ofO(log n) assuming our tree is reasonably balanced.
Storing the height field can be useful for balancing, as inAVL trees.
Storing the size allows ranking (computing the number of elements less than a given target value) and unraking (find an element with a particular rank). Sample code for doing this is given in theAVL tree sample implementation below.
Storing other aggregates like the sum of keys or values allowsrange queries, where we ask, for example, for some aggregate statistic (like the sum or average) of all the elements between some given minimum and maximum.
Assuming we keep the tree balanced and correctly maintain the aggregate data or each subtree, all of these operations can be done inO(log n) time.
Binary search trees are a fine idea, but they only work if they arebalanced—if moving from a tree to its left or right subtree reduces the size by a constant fraction. Balanced binary trees add some extra mechanism to the basic binary search tree to ensure balance. Finding efficient ways to balance a tree has been studied for decades, and several good mechanisms are known. We’ll try to hit the high points of all of them.
The problem is that as we insert new nodes, some paths through the tree may become very long. So we need to be able to shrink the long paths by moving nodes elsewhere in the tree.
But how do we do this? The idea is to notice that there may be many binary search trees that contain the same data, and that we can transform one into another by a local modification called arotation:
y x / \ <==> / \ x C A y / \ / \A B B CSingle rotation on x-y edgeIfA < x < B < y < C, then both versions of this tree have the binary search tree property. By doing the rotation in one direction, we moveA up andC down; in the other direction, we moveA down andC up. So rotations can be used to transfer depth from the leftmost grandchild of a node to the rightmost and vice versa.
But what if it’s the middle grandchildB that’s the problem? A single rotation as above doesn’t moveB up or down. To moveB, we have to reposition it so that it’s on the end of something. We do this by splittingB into two subtreesB1 andB2, and doing two rotations that split the two subtrees while moving both up. For this we need to do two rotations:
z z y / \ ===> / \ ===> / \ x C y C x z / \ / \ /| |\A y x B2 A B1 B2 C / \ / \ B1 B2 A B1Double rotation: rotate xy then zyRotations in principle let us rebalance a tree, but we still need to decide when to do them. If we try to keep the tree in perfect balance (all paths nearly the same length), we’ll spend so much time rotating that we won’t be able to do anything else. So we need a scheme that keeps a tree balanced enough that we get paths of lengthO(log n) while still doingO(log n) rotations per operations.
One of the simplest balanced tree data structures is thetreap, a randomized tree that combines the invariants of a binary search tree and a heap. The idea is to give each node two keys: a tree key used for searching, and a heap key, which is chosen at random and used to decide which nodes rise to the top. If we ignore the heap keys, a treap is a binary search tree: the tree keys store the contents of the tree, and the tree key in each node is greater than all keys in its left subtree and less than all keys in its right subtree. If we ignore the tree keys: a treap is a heap: each node has a heap key that is larger than all heap keys in is descendants.
It is not immediately obvious that we can preserve both invariants at once, but if we are presented with an unorganized pile of nodes already assigned tree and heap keys, we can build a treap by making the node with the largestheap key the root, putting all the nodes with smallertree heap keys in the left subtree and all the nodes with largertree keys in the right subtree, then organizing both subtrees recursively in the same way. This tells use that for any assignment of tree and heap keys, a treap exists (and is in fact unique!), so the only trick is to make sure that it doesn’t cost too much to preserve the treap property when we insert a new element.
To insert a new node, we assign it a random heap key and insert it at a leaf using the usual method for a binary search tree. Because the new node’s random key may be large, this may violate the heap property. But we can rotate it up until the heap property is restored.
For deletions, we first have to search for a node with the key we want to delete, then remove it from the tree. If the node has has most one child, we can just patch it out, by changing its parent’s pointer to point to the child (or to null, if there is no child). If the node has two children, we pick the bigger one and rotate it up. Repeating this process will eventually rotate the node we are deleting down until it either has one child or is a leaf.
It’s not hard to see that the cost of any of these operations is proportional to length of some path in the treap. If all nodes have random heap keys, then the root node will be equally likely to be any of the nodes. This doesn’t guarantee that we get a good split, but a very bad split requires picking a root node close to one side or the other, which is unlikely. People smarter than I am have analyzed the expected height of a tree constructed in this way and show that the length of the longest path converges to(4.311…)log n in the limit (Devroye, A note on the height of binary search trees, JACM 1986; the upper bound is due to Robson, The height of binary search trees, Australian Computer Journal, 1979). This gives anO(log n) bound for the expected height in practice. However, we do have to be careful to make sure that whoever is supplying our inputs can’t see what heap keys we pick, or they will be able to generate an unbalanced tree by repeatedly inserting and deleting nodes until they draw bad heap keys.
Below is an example of a binary search tree implemented as a treap. You can also find much more about treaps on the web page ofCecilia Aragon, one of the inventors of treaps.
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<time.h>#include<limits.h>#define NUM_CHILDREN (2)#define LEFT (0)#define RIGHT (1)// Invariants:// - Every key below child[LEFT] < key < every key below child[RIGHT]// - Every heapKey in both subtreaps < heapKey.// heapKeys are chosen randomly to ensure balance with high probability.struct treap {int key;int heapKey;struct treap *child[NUM_CHILDREN];};voidtreapDestroy(struct treap *t){if(t) {for(int dir = LEFT; dir <= RIGHT; dir++) { treapDestroy(t->child[dir]); } free(t); }}voidtreapPrintHelper(conststruct treap *t,int depth){if(t ==0) {return; } treapPrintHelper(t->child[LEFT], depth+1);// print indented rootfor(int i =0; i < depth; i++) { putchar(' '); } printf("%d [%d]\n", t->key, t->heapKey); treapPrintHelper(t->child[RIGHT], depth+1);}voidtreapPrint(conststruct treap *t){ treapPrintHelper(t,0);}// return 1 if it finds key, 0 otherwiseinttreapSearch(conststruct treap *t,int key){if(t ==0) {// no key!return0; }elseif(key == t->key) {// found itreturn1; }elseif(key < t->key) {// look in leftreturn treapSearch(t->child[LEFT], key); }else {// look in rightreturn treapSearch(t->child[RIGHT], key); }}// return largest element <= key// or INT_MIN if there is no such element.inttreapSearchMaxLE(conststruct treap *t,int key){if(t ==0) {// no key!return INT_MIN; }elseif(key == t->key) {// found itreturn key; }elseif(key < t->key) {// look in leftreturn treapSearchMaxLE(t->child[LEFT], key); }else {// look in rightint result = treapSearchMaxLE(t->child[RIGHT], key);if(result == INT_MIN) {// didn't find itreturn t->key; }else {return result; } }}// rotate the treap pointed to by parent// so that child in direction moves upvoidtreapRotateUp(struct treap **parent,int dir){// get pointers to anything that might move assert(parent);struct treap *child = *parent; assert(child);struct treap *grandchild = child->child[dir]; assert(grandchild);struct treap *middleSubtreap = grandchild->child[!dir];// do the move *parent = grandchild; grandchild->child[!dir] = child; child->child[dir] = middleSubtreap;}// insert key into treap pointed to by parent// if not already presentvoidtreapInsert(struct treap **parent,int key){if(*parent ==0) {// no key! *parent = malloc(sizeof(struct treap)); (*parent)->key = key; (*parent)->heapKey = rand(); (*parent)->child[LEFT] = (*parent)->child[RIGHT] =0; }elseif(key == (*parent)->key) {// found itreturn; }elseif(key < (*parent)->key) {// look in left treapInsert(&(*parent)->child[LEFT], key); }else {// look in right treapInsert(&(*parent)->child[RIGHT], key); }// check heap propertyfor(int dir = LEFT; dir <= RIGHT; dir++) {if((*parent)->child[dir] !=0 && (*parent)->child[dir]->heapKey > (*parent)->heapKey) { treapRotateUp(parent, dir); } }}// delete a node from a treap (if present)voidtreapDelete(struct treap **parent,int key){// first we look for itif(*parent ==0) {// not therereturn; }elseif(key == (*parent)->key) {// got it; rotate down until we have a missing kidfor(;;) {// do we have a missing child?for(int dir = LEFT; dir <= RIGHT; dir++) {if((*parent)->child[dir] ==0) {// yes; free this node and promote other kidstruct treap *toDelete = *parent; *parent = toDelete->child[!dir]; free(toDelete);return; } }// no missing child, have to rotate downint biggerKid;if((*parent)->child[LEFT]->heapKey > (*parent)->child[RIGHT]->heapKey) { biggerKid = LEFT; }else { biggerKid = RIGHT; }// rotate up the bigger kid treapRotateUp(parent, biggerKid);// node to delete is now on opposite side parent = &(*parent)->child[!biggerKid]; } }else { treapDelete(&(*parent)->child[key < (*parent)->key ? LEFT : RIGHT], key); }}#define TEST_THRESHOLD (10)intmain(int argc,char **argv){if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; }struct treap *t =0;int key;while(scanf("%d", &key) ==1) {if(key >=0) { treapInsert(&t, key); }else { treapDelete(&t, -key); } treapPrint(t); printf("--- largest <= %d is %d\n", TEST_THRESHOLD, treapSearchMaxLE(t, TEST_THRESHOLD)); } treapDestroy(t);return0;}AVL trees solve the balancing problem by enforcing the invariant that the heights of the two subtrees sitting under each node differ by at most one. This does not guarantee perfect balance, but it does get close. LetS(k) be the size of the smallest AVL tree with heightk. This tree will have at least one subtree of heightk − 1, but its other subtree can be of heightk − 2 (and should be, to keep it as small as possible). We thus have the recurrenceS(k) = 1 + S(k − 1) + S(k − 2), which is very close to the Fibonacci recurrence.
It is possible to solve this exactly using generating functions. But we can get close by guessing thatS(k) ≥ ak for some constanta. This clearly works forS(0) = a0 = 1. For largerk, compute
This last quantity is at leastak provided(1/a + 1/a2) is at least 1. We can solve exactly for the largesta that makes this work, but a very quick calculation shows thata = 3/2 works:2/3 + 4/9 = 10/9 > 1. It follows that any AVL tree with heightk has at least(3/2)k nodes, or conversely that any AVL tree with(3/2)k nodes has height at mostk. So the height of an arbitrary AVL tree withn nodes is no greater thanlog3/2n = O(log n).
How do we maintain this invariant? The first thing to do is add extra information to the tree, so that we can tell when the invariant has been violated. AVL trees store in each node the difference between the heights of its left and right subtrees, which will be one of − 1,0, or1. In an ideal world this would requirelog23 ≈ 1.58 bits per node, but since fractional bits are difficult to represent on modern computers a typical implementation uses two bits. Inserting a new node into an AVL tree involves
Implementing this correctly is tricky. Intuitively, we can imagine a version of an AVL tree in which we stored the height of each node (usingO(log log n) bits). When we insert a new node, only the heights of its ancestors change—so step 2 requires updatingO(log n) height fields. Similarly, it is only these ancestors that can be too tall. It turns out that fixing the closest ancestor fixes all the ones above it (because it shortens their longest paths by one as well). So just one single or double rotation restores balance.
Deletions are also possible, but are uglier: a deletion in an AVL tree may require as many asO(log n) rotations. The basic idea is to use the standardbinary search tree deletion trick of either splicing out a node if it has no right child, or replacing it with the minimum value in its right subtree (the node for which is spliced out); we then have to check to see if we need to rebalance at every node above whatever node we removed.
Which rotations we need to do to rebalance depends on how some pair of siblings are unbalanced. Below, we show the possible cases.
Zig-zig case: This can occur after inserting in A or deleting in C. Here we rotate A up:
y x / \ ===> / \ x C A y / \ | / \A B # B C|#Zig-zag case: This can occur after inserting in B or deleting in C. This requires a double rotation.
z z y / \ ===> / \ ===> / \ x C y C x z / \ / \ /| |\A y x B2 A B1 B2 C / \ / \ B1 B2 A B1Zig-zag case, again: This last case comes up after deletion if both nephews of the short node are too tall. The same double rotation we used in the previous case works here, too. Note that one of the subtrees is still one taller than the others, but that’s OK.
z z y / \ ===> / \ ===> / \ x C y C x z / \ / \ /| |\A y x B2 A B1 B2 C| / \ / \ |# B1 B2 A B1 # | #If we are not fanatical about space optimization, we can just keep track of the heights of all nodes explicitly, instead of managing the − 1, 0, 1 balance values. Below, we give a not-very-optimized example implementation that uses this approach to store a set ofints. This is pretty much our standard unbalanced BST (although we have to make sure that the insert and delete routines are recursive, so that we can fix things up on the way back out), with a layer on top, implemented in thetreeFix function, that tracks the height and size of each subtree (although we don’t use size), and another layer on top of that, implemented in thetreeBalance function, that fixes any violations of the AVL balance rule.
/* * Basic binary search tree data structure without balancing info. * * Convention: * * Operations that update a tree are passed a struct tree **, * so they can replace the argument with the return value. * * Operations that do not update the tree get a const struct tree *. */#define LEFT (0)#define RIGHT (1)#define TREE_NUM_CHILDREN (2)struct tree {/* we'll make this an array so that we can make some operations symmetric */struct tree *child[TREE_NUM_CHILDREN];int key;int height;/* height of this node */size_t size;/* size of subtree rooted at this node */};#define TREE_EMPTY (0)#define TREE_EMPTY_HEIGHT (-1)/* free all elements of a tree, replacing it with TREE_EMPTY */void treeDestroy(struct tree **root);/* insert an element into a tree pointed to by root */void treeInsert(struct tree **root,int newElement);/* return 1 if target is in tree, 0 otherwise *//* we allow root to be modified to allow for self-balancing trees */int treeContains(conststruct tree *root,int target);/* delete minimum element from the tree and return its key *//* do not call this on an empty tree */int treeDeleteMin(struct tree **root);/* delete target from the tree *//* has no effect if target is not in tree */void treeDelete(struct tree **root,int target);/* return height of tree */int treeHeight(conststruct tree *root);/* return size of tree */size_t treeSize(conststruct tree *root);/* pretty-print the contents of a tree */void treePrint(conststruct tree *root);/* return the number of elements in tree less than target */size_t treeRank(conststruct tree *root,int target);/* return an element with the given rank *//* rank must be less than treeSize(root) */int treeUnrank(conststruct tree *root,size_t rank);/* check that aggregate data is correct throughout the tree */void treeSanityCheck(conststruct tree *root);#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<stdint.h>#include<stdlib.h>#include"tree.h"inttreeHeight(conststruct tree *root){if(root ==0) {return TREE_EMPTY_HEIGHT; }else {return root->height; }}/* recompute height from height of kids */staticinttreeComputeHeight(conststruct tree *root){int childHeight;int maxChildHeight;int i;if(root ==0) {return TREE_EMPTY_HEIGHT; }else { maxChildHeight = TREE_EMPTY_HEIGHT;for(i =0; i < TREE_NUM_CHILDREN; i++) { childHeight = treeHeight(root->child[i]);if(childHeight > maxChildHeight) { maxChildHeight = childHeight; } }return maxChildHeight +1; }}size_ttreeSize(conststruct tree *root){if(root ==0) {return0; }else {return root->size; }}/* recompute size from size of kids */staticinttreeComputeSize(conststruct tree *root){int size;int i;if(root ==0) {return0; }else { size =1;for(i =0; i < TREE_NUM_CHILDREN; i++) { size += treeSize(root->child[i]); }return size; }}/* fix aggregate data in root *//* assumes children are correct */staticvoidtreeAggregateFix(struct tree *root){if(root) { root->height = treeComputeHeight(root); root->size = treeComputeSize(root); }}/* rotate child in given direction to root */staticvoidtreeRotate(struct tree **root,int direction){struct tree *x;struct tree *y;struct tree *b;/* * y x * / \ / \ * x C <=> A y * / \ / \ * A B B C */ y = *root; assert(y); x = y->child[direction]; assert(x); b = x->child[!direction];/* do the rotation */ *root = x; x->child[!direction] = y; y->child[direction] = b;/* fix aggregate data for y then x */ treeAggregateFix(y); treeAggregateFix(x);}/* restore AVL property at *root after an insertion or deletion *//* assumes subtrees already have AVL property */staticvoidtreeRebalance(struct tree **root){int tallerChild;if(*root) {for(tallerChild =0; tallerChild < TREE_NUM_CHILDREN; tallerChild++) {if(treeHeight((*root)->child[tallerChild]) >= treeHeight((*root)->child[!tallerChild]) +2) {/* which grandchild is the problem? */if(treeHeight((*root)->child[tallerChild]->child[!tallerChild]) > treeHeight((*root)->child[tallerChild]->child[tallerChild])) {/* opposite-direction grandchild is too tall *//* rotation at root will just change its parent but not change height *//* so we rotate it up first */ treeRotate(&(*root)->child[tallerChild], !tallerChild); }/* now rotate up the taller child */ treeRotate(root, tallerChild);/* don't bother with other child */break; } }/* test that we actually fixed it */ assert(abs(treeHeight((*root)->child[LEFT]) - treeHeight((*root)->child[RIGHT])) <=1);#ifdef PARANOID_REBALANCE treeSanityCheck(*root);#endif }}/* free all elements of a tree, replacing it with TREE_EMPTY */voidtreeDestroy(struct tree **root){int i;if(*root) {for(i =0; i < TREE_NUM_CHILDREN; i++) { treeDestroy(&(*root)->child[i]); } free(*root); *root = TREE_EMPTY; }}/* insert an element into a tree pointed to by root */voidtreeInsert(struct tree **root,int newElement){struct tree *e;if(*root ==0) {/* not already there, put it in */ e = malloc(sizeof(*e)); assert(e); e->key = newElement; e->child[LEFT] = e->child[RIGHT] =0; *root = e; }elseif((*root)->key == newElement) {/* already there, do nothing */return; }else {/* do this recursively so we can fix data on the way back out */ treeInsert(&(*root)->child[(*root)->key < newElement], newElement); }/* fix the aggregate data */ treeAggregateFix(*root); treeRebalance(root);}/* return 1 if target is in tree, 0 otherwise */inttreeContains(conststruct tree *t,int target){while(t && t->key != target) { t = t->child[t->key < target]; }return t !=0;}/* delete minimum element from the tree and return its key *//* do not call this on an empty tree */inttreeDeleteMin(struct tree **root){struct tree *toFree;int retval; assert(*root);/* can't delete min from empty tree */if((*root)->child[LEFT]) {/* recurse on left subtree */ retval = treeDeleteMin(&(*root)->child[LEFT]); }else {/* delete the root */ toFree = *root; retval = toFree->key; *root = toFree->child[RIGHT]; free(toFree); }/* fix the aggregate data */ treeAggregateFix(*root); treeRebalance(root);return retval;}/* delete target from the tree *//* has no effect if target is not in tree */voidtreeDelete(struct tree **root,int target){struct tree *toFree;/* do nothing if target not in tree */if(*root) {if((*root)->key == target) {if((*root)->child[RIGHT]) {/* replace root with min value in right subtree */ (*root)->key = treeDeleteMin(&(*root)->child[RIGHT]); }else {/* patch out root */ toFree = *root; *root = toFree->child[LEFT]; free(toFree); } }else { treeDelete(&(*root)->child[(*root)->key < target], target); }/* fix the aggregate data */ treeAggregateFix(*root); treeRebalance(root); }}/* how far to indent each level of the tree */#define INDENTATION_LEVEL (2)/* print contents of a tree, indented by depth */staticvoidtreePrintIndented(conststruct tree *root,int depth){int i;if(root !=0) { treePrintIndented(root->child[LEFT], depth+1);for(i =0; i < INDENTATION_LEVEL*depth; i++) { putchar(' '); } printf("%d Height: %d Size: %zu (%p)\n", root->key, root->height, root->size, (void *) root); treePrintIndented(root->child[RIGHT], depth+1); }}/* print the contents of a tree */voidtreePrint(conststruct tree *root){ treePrintIndented(root,0);}size_ttreeRank(conststruct tree *t,int target){size_t rank =0;while(t && t->key != target) {if(t->key < target) {/* go right *//* root and left subtree are all less than target */ rank += (1 + treeSize(t->child[LEFT])); t = t->child[RIGHT]; }else {/* go left */ t = t->child[LEFT]; } }/* we must also count left subtree */return rank + treeSize(t->child[LEFT]);}inttreeUnrank(conststruct tree *t,size_t rank){size_t leftSize;/* basic idea: if rank < treeSize(child[LEFT]), recurse in left child *//* if it's equal, return the root *//* else recurse in right child with rank = rank - treeSize(child[LEFT]) - 1 */while(rank != (leftSize = treeSize(t->child[LEFT]))) {if(rank < leftSize) { t = t->child[LEFT]; }else { t = t->child[RIGHT]; rank -= (leftSize +1); } }return t->key;}/* check that aggregate data is correct throughout the tree */voidtreeSanityCheck(conststruct tree *root){int i;if(root) { assert(root->height == treeComputeHeight(root)); assert(root->size == treeComputeSize(root)); assert(abs(treeHeight(root->child[LEFT]) - treeHeight(root->child[RIGHT])) <2);for(i =0; i < TREE_NUM_CHILDREN; i++) { treeSanityCheck(root->child[i]); } }}#ifdef TEST_MAINintmain(int argc,char **argv){int key;int i;constint n =10;constint randRange =1000;constint randTrials =10000;struct tree *root = TREE_EMPTY;if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; }/* original test */for(i =0; i < n; i++) { assert(!treeContains(root, i)); treeInsert(&root, i); assert(treeContains(root, i)); treeSanityCheck(root);#ifdef PRINT_AFTER_OPERATIONS treePrint(root); puts("---");#endif }/* check ranks */for(i =0; i < n; i++) { assert(treeRank(root, i) == i); assert(treeUnrank(root, i) == i); } treeSanityCheck(root);/* now delete everything */for(i =0; i < n; i++) { assert(treeContains(root, i)); treeDelete(&root, i); assert(!treeContains(root, i)); treeSanityCheck(root);#ifdef PRINT_AFTER_OPERATIONS treePrint(root); puts("---");#endif } treeSanityCheck(root); treeDestroy(&root);/* random test */ srand(1);for(i =0; i < randTrials; i++) { treeInsert(&root, rand() % randRange); treeDelete(&root, rand() % randRange); } treeSanityCheck(root); treeDestroy(&root);#ifdef TEST_USE_STDINwhile(scanf("%d", &key) ==1) {/* insert if positive, delete if negative */if(key >0) { treeInsert(&root, key); assert(treeContains(root, key)); }elseif(key <0) { treeDelete(&root, -key); assert(!treeContains(root, key)); }/* else ignore 0 */#ifdef PRINT_AFTER_OPERATIONS treePrint(root); puts("---");#endif } treeSanityCheck(root); treeDestroy(&root);#endif/* TEST_USE_STDIN */return0;}#endif/* TEST_MAIN */ThisMakefile will compile and run some demo code intree.c if run withmake test.
(An older implementation can be found in the directoryexamples/trees/oldAvlTree.)
An early branch in the evolution of balanced trees was the 2–3 tree. Here all paths have the same length, but internal nodes have either 2 or 3 children. So a 2–3 tree with heightk has between2k and3k leaves and a comparable number of internal nodes. The maximum path length in a tree withn nodes is at most⌈lg n⌉, as in a perfectly balanced binary tree.
An internal node in a 2–3 tree holds one key if it has two children (including two nil pointers) and two if it has three children. A search that reaches a three-child node must compare the target with both keys to decide which of the three subtrees to recurse into. As in binary trees, these comparisons take constant time, so we can search a 2–3 tree inO(log n) time.
Insertion is done by expanding leaf nodes. This may cause a leaf to split when it acquires a third key. When a leaf splits, it becomes two one-key nodes and the middle key moves up into its parent. This may cause further splits up the ancestor chain; the tree grows in height by adding a new root when the old root splits. In practice only a small number of splits are needed for most insertions, but even in the worst case this entire process takesO(log n) time.
It follows that 2–3 trees have the same performance as AVL trees. Conceptually, they are simpler, but having to write separate cases for 2-child and 3-child nodes doubles the size of most code that works on 2–3 trees. The real significance of 2–3 trees is as a precursor to two other kinds of trees, thered-black tree and theB-tree.
A red-black tree is a 2–3–4 tree (i.e. all nodes have 2, 3, or 4 children and 1, 2, or 3 internal keys) where each node is represented by a little binary tree with a black root and zero, one, or two red extender nodes as follows:
The invariant for a red-black tree is that
For technical reasons, we include the null pointers at the bottom of the tree as black nodes; this has no effect on the invariant, but simplifies the description of the rebalancing procedure.
From the invariant it follows that every path has betweenk and2k nodes, wherek is theblack-height, the common number of black nodes on each path. From this we can prove that the height of the tree isO(log n).
Searching in a red-black tree is identical to searching in any other binary search tree; we simply ignore the color bit on each node. So search takesO(log n) time. For insertions, we use the standard binary search tree insertion algorithm, and insert the new node as a red node. This may violate the first part of the invariant (it doesn’t violate the second because it doesn’t change the number of black nodes on any path). In this case we need to fix up the constraint by recoloring nodes and possibly performing a single or double rotation.
Which operations we need to do depend on the color of the new node’s uncle. If the uncle is red, we can recolor the node’s parent, uncle, and grandparent and get rid of the double-red edge between the new node and its parent without changing the number of black nodes on any path. In this case, the grandparent becomes red, which may create a new double-red edge which must be fixed recursively. Thus up toO(log n) such recolorings may occur at a total cost ofO(log n).
If the uncle is black (which includes the case where the uncle is a null pointer), a rotation (possibly a double rotation) and recoloring is necessary. In this case (depicted at the bottom of the picture above), the new grandparent is always black, so there are no more double-red edges. So at most two rotations occur after any insertion.
Deletion is more complicated but can also be done inO(log n) recolorings andO(1) (in this case up to 3) rotations. Because deletion is simpler in red-black trees than in AVL trees, and because operations on red-black trees tend to have slightly smaller constants than corresponding operation on AVL trees, red-black trees are more often used that AVL trees in practice.
Neither is used as much as a B-tree, a specialized data structure optimized for storage systems where the cost of reading or writing a large block (of typically 4096 or 8192 bytes) is no greater than the cost of reading or writing a single bit. Such systems include typical disk drives, where the disk drive has to spend so long finding data on disk that it tries to amortize the huge (tens of millions of CPU clock cycles) seek cost over many returned bytes.
A B-tree is a generalization of a 2–3 tree where the root has at mostM − 1 keys, and each node has betweenM/2 andM − 1 keys, whereM is some large constant chosen so that a node (including up toM pointers and up toM − 1 keys) will just fit inside a single block. When a node would otherwise end up withM keys, it splits into two nodes withM/2 keys each, and moves its middle key up into its parent. As in 2–3 trees this may eventually require the root to split and a new root to be created; in practice,M is often large enough that a small fixed height is enough to span as much data as the storage system is capable of holding.
Searches in B-trees require looking throughlogMn nodes, at a cost ofO(M) time per node. IfM is a constant the total time is asymptoticallyO(log n). But the reason for using B-trees is that theO(M) cost of reading a block is trivial compare to the much larger constant time to find the block on the disk; and so it is better to minimize the number of disk accesses (by makingM large) than reduce the CPU time.
Deletions in B-trees are a nuisance. Deleting a key within a node that has onlyM/2 keys requires arerotation that steals a key from one of its siblings. If this would cause the sibling to drop belowM/2 as well, the two nodes are merged. Merging two siblings steals a key from the parent, which may require further tinkering higher up in the tree. This means that deleting a key from a B-tree will require modifyingO(log n) nodes in the worst case.
Below is a rudimentary B-tree implementation that provides only insert and search. For practical use you should probably for an existing B-tree library that has been tuned for whatever programming environment and system you are working on.
/* implementation of a B-tree */typedefstruct btNode *bTree;/* create a new empty tree */bTree btCreate(void);/* free a tree */void btDestroy(bTree t);/* return nonzero if key is present in tree */int btSearch(bTree t,int key);/* insert a new element into a tree */void btInsert(bTree t,int key);/* print all keys of the tree in order */void btPrintKeys(bTree t);#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<string.h>#include"bTree.h"#define MAX_KEYS (1024)struct btNode {int isLeaf;/* is this a leaf node? */int numKeys;/* how many keys does this node contain? */int keys[MAX_KEYS];struct btNode *kids[MAX_KEYS+1];/* kids[i] holds nodes < keys[i] */};bTreebtCreate(void){ bTree b; b = malloc(sizeof(*b)); assert(b); b->isLeaf =1; b->numKeys =0;return b;}voidbtDestroy(bTree b){int i;if(!b->isLeaf) {for(i =0; i < b->numKeys +1; i++) { btDestroy(b->kids[i]); } } free(b);}/* return smallest index i in sorted array such that key <= a[i] *//* (or n if there is no such index) */staticintsearchKey(int n,constint *a,int key){int lo;int hi;int mid;/* invariant: a[lo] < key <= a[hi] */ lo = -1; hi = n;while(lo +1 < hi) { mid = (lo+hi)/2;if(a[mid] == key) {return mid; }elseif(a[mid] < key) { lo = mid; }else { hi = mid; } }return hi;}intbtSearch(bTree b,int key){int pos;/* have to check for empty tree */if(b->numKeys ==0) {return0; }/* look for smallest position that key fits below */ pos = searchKey(b->numKeys, b->keys, key);if(pos < b->numKeys && b->keys[pos] == key) {return1; }else {return(!b->isLeaf && btSearch(b->kids[pos], key)); }}/* insert a new key into a tree *//* returns new right sibling if the node splits *//* and puts the median in *median *//* else returns 0 */static bTreebtInsertInternal(bTree b,int key,int *median){int pos;int mid; bTree b2; pos = searchKey(b->numKeys, b->keys, key);if(pos < b->numKeys && b->keys[pos] == key) {/* nothing to do */return0; }if(b->isLeaf) {/* everybody above pos moves up one space */ memmove(&b->keys[pos+1], &b->keys[pos],sizeof(*(b->keys)) * (b->numKeys - pos)); b->keys[pos] = key; b->numKeys++; }else {/* insert in child */ b2 = btInsertInternal(b->kids[pos], key, &mid);/* maybe insert a new key in b */if(b2) {/* every key above pos moves up one space */ memmove(&b->keys[pos+1], &b->keys[pos],sizeof(*(b->keys)) * (b->numKeys - pos));/* new kid goes in pos + 1*/ memmove(&b->kids[pos+2], &b->kids[pos+1],sizeof(*(b->kids)) * (b->numKeys - pos)); b->keys[pos] = mid; b->kids[pos+1] = b2; b->numKeys++; } }/* we waste a tiny bit of space by splitting now * instead of on next insert */if(b->numKeys >= MAX_KEYS) { mid = b->numKeys/2; *median = b->keys[mid];/* make a new node for keys > median *//* picture is: * * 3 5 7 * A B C D * * becomes * (5) * 3 7 * A B C D */ b2 = malloc(sizeof(*b2)); b2->numKeys = b->numKeys - mid -1; b2->isLeaf = b->isLeaf; memmove(b2->keys, &b->keys[mid+1],sizeof(*(b->keys)) * b2->numKeys);if(!b->isLeaf) { memmove(b2->kids, &b->kids[mid+1],sizeof(*(b->kids)) * (b2->numKeys +1)); } b->numKeys = mid;return b2; }else {return0; }}voidbtInsert(bTree b,int key){ bTree b1;/* new left child */ bTree b2;/* new right child */int median; b2 = btInsertInternal(b, key, &median);if(b2) {/* basic issue here is that we are at the root *//* so if we split, we have to make a new root */ b1 = malloc(sizeof(*b1)); assert(b1);/* copy root to b1 */ memmove(b1, b,sizeof(*b));/* make root point to b1 and b2 */ b->numKeys =1; b->isLeaf =0; b->keys[0] = median; b->kids[0] = b1; b->kids[1] = b2; }}Yet another approach to balancing is to do it dynamically. Splay trees, described by Sleator and Tarjan in the paper “Self-adjusting binary search trees” (JACM 32(3):652–686, July 1985) are binary search trees in which every search operation rotates the target to the root. If this is done correctly, theamortized cost of each tree operation isO(log n), although particular rare operations might take as much asO(n) time. Splay trees require no extra space because they store no balancing information; however, the constant factors on searches can be larger because every search requires restructuring the tree. For some applications this additional cost is balanced by the splay tree’s ability to adapt to data access patterns; if some elements of the tree are hit more often than others, these elements will tend to migrate to the top, and the cost of a typical search will drop toO(log m), wherem is the size of the “working set” of frequently-accessed elements.
The basic idea of a splay operation is that we move some particular node to the root of the tree, using a sequence of rotations that tends to fix the balance of the tree if the node starts out very deep. So while we might occasionally drive the tree into a state that is highly unbalanced, as soon as we try to exploit this by searching for a deep node, we’ll start balancing the tree so that we can’t collect too much additional cost. In fact, in order to set up the bad state in the first place we will have to do a lot of cheap splaying operations: the missing cost of these cheap splays ends up paying for the cost of the later expensive search.
Splaying a node to the root involves performing rotations two layers at a time. There are two main cases, depending on whether the node’s parent and grandparent are in the same direction (zig-zig) or in opposite directions (zig-zag), plus a third case when the node is only one step away from the root. At each step, we pick one of these cases and apply it, until the target node reaches the root of the tree.
This is probably best understood by looking at a figure from the original paper:
The bottom two cases are the ones we will do most of the time.
Just looking at the picture, it doesn’t seem like zig-zig will improve balance much. But if we have a long path made up of zig-zig cases, each operation will push at least one node off of this path, cutting the length of the path in half. So the rebalancing happens as much because we are pushing nodes off of the long path as because the specific rotation operations improve things locally.
Sleator and Tarjan show that any sequence ofm splay operations on ann-node splay tree has total cost at mostO((m + n)log n + m). For largem (at least linear inn), theO(mlog n) term dominates, giving an amortized cost per operation ofO(log n), the same as we get from any balanced binary tree. This immediately gives a bound on search costs, because the cost of plunging down the tree to find the node we are looking for is proportional to the cost of splaying it up to the root.
Splay trees have a useful “caching” property in that they pull frequently-accessed nodes to the to the top and push less-frequently-accessed nodes down. The authors show that if onlyk of then nodes are accessed, the long-run amortized cost per search drops toO(log k). For more general access sequences, it is conjectured that the cost to perform a sufficiently long sequence of searches using a splay tree is in fact optimal up to a constant factor (the “dynamic optimality conjecture”), but no one has yet been able to prove this conjecture (or provide a counterexample).21
A search operation consists of a standard binary tree search followed by splaying the target node to the root (if present) or the last non-null node we reached to the root instead (if not).
Insertion and deletion are built on top of procedures to split and join trees.
A split divides a single splay tree into two splay trees, consisting of all elements less than or equal to some valuex and all elements greater thanx. This is done by searching forx, which brings eitherx or the first element less than or greater thanx to the root, then breaking the link between the root and its left or right child depending on whether the root should go in the right or left tree.
A join merges two splay treesL andR, where every element inL is less than every element inR. This involves splaying the largest element inL to the root, and then making the root ofR the right child of this element.
To do an insert ofx, we do a split aroundx, then make the roots of the two trees the children of a new element holdingx (unlessx is already present in the tree, in which case we stop before breaking the trees apart).
To do a delete of an elementx, we splayx to the root, remove it, then join the two orphaned subtrees.
For each operation, we are doing a constant number of splays (amortized costO(log n) each), plusO(1) additional work. A bit of work is needed to ensure that the joins and splits don’t break the amortized cost analysis, but this is done in the paper, so we will sweep it under the carpet with the rest of the analysis.
There are a few remaining details that we need to deal with before trying to implement a splay trees. Because the splay tree could become very deep, we probably don’t want to implement a splay recursively in a language like C, because we’ll blow out our stack. We also have a problem if we are trying to rotate our target up from the bottom of figuring out what its ancestors are. We could solve both of these problems by including parent pointers in our tree, but this would add a lot of complexity and negate the space improvement over AVL trees of not having to store heights.
The solution given in the Sleator-Tarjan paper is to replace the bottom-up splay procedure with a top-down splay procedure that accomplishes the same task. The idea is that rotating a node up from the bottom effectively splits the tree above it into two new left and right subtrees by pushing ancestors sideways according to the zig-zig and zig-zag patters. But we can recognize these zig-zig and zig-zag patterns from the top as well, and so we can construct these same left and right subtrees from the top down instead of the bottom up. When we do this, instead of adding new nodes to the tops of the trees, we will be adding new nodes to the bottoms, as the right child of the rightmost node in the left tree or the left child of the rightmost node in the left tree.
Here’s the picture, from the original paper:
To implement this, we need to keep track of the roots of the three trees, as well as the locations in the left and right trees where we will be adding new vertices. The roots we can just keep pointers to. For the lower corners of the trees, it makes sense to store instead a pointer to the pointer location, so that we can modify the pointer in the tree (and then move the pointer to point to the pointer in the new corner). Initially, these corner pointers will just point to the left and right tree roots, which will start out empty.
The last step (shown as Figure 12 from the paper) pastes the tree back together by inserting the left and right trees between the new root and its children.
Here is an implementation of a splay tree, with an interface similar to the previousAVL tree implementation.
/* * Basic binary search tree data structure without balancing info. * * Convention: * * Operations that update a tree are passed a struct tree **, * so they can replace the argument with the return value. * * Operations that do not update the tree get a const struct tree *. */#define LEFT (0)#define RIGHT (1)#define TREE_NUM_CHILDREN (2)struct tree {/* we'll make this an array so that we can make some operations symmetric */struct tree *child[TREE_NUM_CHILDREN];int key;};#define TREE_EMPTY (0)/* free all elements of a tree, replacing it with TREE_EMPTY */void treeDestroy(struct tree **root);/* insert an element into a tree pointed to by root */void treeInsert(struct tree **root,int newElement);/* return 1 if target is in tree, 0 otherwise *//* we allow root to be modified to allow for self-balancing trees */int treeContains(struct tree **root,int target);/* delete target from the tree *//* has no effect if target is not in tree */void treeDelete(struct tree **root,int target);/* pretty-print the contents of a tree */void treePrint(conststruct tree *root);#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<limits.h>#include"tree.h"/* free all elements of a tree, replacing it with TREE_EMPTY */voidtreeDestroy(struct tree **root){/* we want to avoid doing this recursively, because the tree might be deep *//* so we will repeatedly delete the root until the tree is empty */while(*root) { treeDelete(root, (*root)->key); }}/* rotate child in given direction to root */voidtreeRotate(struct tree **root,int direction){struct tree *x;struct tree *y;struct tree *b;/* * y x * / \ / \ * x C <=> A y * / \ / \ * A B B C */ y = *root; assert(y); x = y->child[direction]; assert(x); b = x->child[!direction];/* do the rotation */ *root = x; x->child[!direction] = y; y->child[direction] = b;}/* link operations for top-down splay *//* this pastes a node in as !d-most node in subtree on side d */staticinlinevoidtreeLink(struct tree ***hook,int d,struct tree *node){ *hook[d] = node;/* strictly speaking we don't need to do this, but it allows printing the partial trees */ node->child[!d] =0; hook[d] = &node->child[!d];}/* splay last element on path to target to root */staticvoidtreeSplay(struct tree **root,int target){struct tree *t;struct tree *child;struct tree *grandchild;struct tree *top[TREE_NUM_CHILDREN];/* accumulator trees that will become subtrees of new root */struct tree **hook[TREE_NUM_CHILDREN];/* where to link new elements into accumulator trees */int d;int dChild;/* direction of child */int dGrandchild;/* direction of grandchild *//* we don't need to keep following this pointer, we'll just fix it at the end */ t = *root;/* don't do anything to an empty tree */if(t ==0) {return; }/* ok, tree is not empty, start chopping it up */for(d =0; d < TREE_NUM_CHILDREN; d++) { top[d] =0; hook[d] = &top[d]; }/* keep going until we hit the key or we would hit a null pointer in the child */while(t->key != target && (child = t->child[dChild = t->key < target]) !=0) {/* child is not null */ grandchild = child->child[dGrandchild = child->key < target];#ifdef DEBUG_SPLAY treePrint(top[0]); puts("---"); treePrint(t); puts("---"); treePrint(top[1]); puts("===");#endifif(grandchild ==0 || child->key == target) {/* zig case; paste root into opposite-side hook */ treeLink(hook, !dChild, t); t = child;/* we can break because we know we will hit child == 0 next */break; }elseif(dChild == dGrandchild) {/* zig-zig case *//* rotate and then hook up child *//* grandChild becomes new root */ treeRotate(&t, dChild); treeLink(hook, !dChild, child); t = grandchild; }else {/* zig-zag case *//* root goes to !dChild, child goes to dChild, grandchild goes to root */ treeLink(hook, !dChild, t); treeLink(hook, dChild, child); t = grandchild; } }/* now reassemble the tree *//* t's children go in hooks, top nodes become t's new children */for(d =0; d < TREE_NUM_CHILDREN; d++) { *hook[d] = t->child[d]; t->child[d] = top[d]; }/* and put t back in *root */ *root = t;}/* return 1 if target is in tree, 0 otherwise */inttreeContains(struct tree **root,int target){ treeSplay(root, target);return *root !=0 && (*root)->key == target;}/* insert an element into a tree pointed to by root */voidtreeInsert(struct tree **root,int newElement){struct tree *e;struct tree *t;int d;/* which side of e to put old root on */ treeSplay(root, newElement); t = *root;/* skip if already present */if(t && t->key == newElement) {return; }/* otherwise split the tree */ e = malloc(sizeof(*e)); assert(e); e->key = newElement;if(t ==0) { e->child[LEFT] = e->child[RIGHT] =0; }else {/* split tree and put e on top *//* we know t is closest to e, so we don't have to move anything else */ d = (*root)->key > newElement; e->child[d] = t; e->child[!d] = t->child[!d]; t->child[!d] =0; }/* either way we stuff e in *root */ *root = e;}/* delete target from the tree *//* has no effect if target is not in tree */voidtreeDelete(struct tree **root,int target){struct tree *left;struct tree *right; treeSplay(root, target);if(*root && (*root)->key == target) {/* save pointers to kids */ left = (*root)->child[LEFT]; right = (*root)->child[RIGHT];/* free the old root */ free(*root);/* if left is empty, just return right */if(left ==0) { *root = right; }else {/* first splay max element in left to top */ treeSplay(&left, INT_MAX);/* now paste in right subtree */ left->child[RIGHT] = right;/* return left */ *root = left; } }}/* how far to indent each level of the tree */#define INDENTATION_LEVEL (2)/* print contents of a tree, indented by depth */staticvoidtreePrintIndented(conststruct tree *root,int depth){int i;if(root !=0) { treePrintIndented(root->child[LEFT], depth+1);for(i =0; i < INDENTATION_LEVEL*depth; i++) { putchar(' '); } printf("%d (%p)\n", root->key, (void *) root); treePrintIndented(root->child[RIGHT], depth+1); }}/* print the contents of a tree */voidtreePrint(conststruct tree *root){ treePrintIndented(root,0);}#ifdef TEST_MAINintmain(int argc,char **argv){int i;constint n =10;struct tree *root = TREE_EMPTY;if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; }for(i =0; i < n; i++) { assert(!treeContains(&root, i)); treeInsert(&root, i); assert(treeContains(&root, i)); treePrint(root); puts("==="); }/* now delete everything */for(i =0; i < n; i++) { assert(treeContains(&root, i)); treeDelete(&root, i); assert(!treeContains(&root, i)); treePrint(root); puts("==="); } treeDestroy(&root);return0;}#endifMakefile. The filespeedTest.c can be used to do a simple test of the efficiency of inserting many random elements. On my machine, the splay tree version is about 10% slower than the AVL tree for this test on a million elements. This probably indicates a bigger slowdown fortreeInsert itself, because some of the time will be spent inrand andtreeDestroy, but I was too lazy to actually test this further.
For more details on splay trees, seethe original paper, or any number of demos, animations, and other descriptions that can be found viaGoogle.
Scapegoat trees are another amortized balanced tree data structure. The idea of a scapegoat tree is that if we ever find ourselves doing an insert at the end of a path that is too long, we can find some subtree rooted at a node along this path that is particularly imbalanced and rebalance it all at once at a cost ofO(k) wherek is the size of the subtree. These were shown by Galperin and Rivest (SODA 1993) to giveO(log n) amortized cost for inserts, while guaranteeingO(log n) depth, so that inserts also run inO(log n) worst-case time; they also came up with the name “scapegoat tree”, although it turns out the same data structure had previously been published by Andersson in 1989. Unlike splay trees, scapegoat trees do not require modifying the tree during a search, and unlike AVL trees, scapegoat trees do not require tracking any information in nodes (although they do require tracking the total size of the tree and, to allow for rebalancing after many deletes, the maximum size of the tree since the last time the entire tree was rebalanced).
Unfortunately, scapegoat trees are not very fast, so one is probably better off with an AVL tree.
Skip lists are yet another balanced tree data structure, where the tree is disguised as a tower of linked lists. Since they use randomization for balance, we describe them with otherrandomized data structures.
AVL trees and red-black trees have been implemented for every reasonable programming language you’ve ever heard of. For C implementations, a good place to start is athttp://adtinfo.org/.
These are notes on implementinggraphs and graph algorithms in C.
Agraph consists of a set ofnodes orvertices together with a set ofedges orarcs where each edge joins two vertices. Unless otherwise specified, a graph isundirected: each edge is an unordered pair{u, v} of vertices, and we don’t regard either of the two vertices as having a distinct role from the other. However, it is more common in computing to considerdirected graphs ordigraphs in which edges areordered pairs(u, v); here the vertexu is thesource of the edge and vertex v is thesink ortarget of the edge. Directed edges are usually drawn as arrows and undirected edges as curves or line segments. It is always possible to represent an undirected graph as a directed graph where each undirected edge{u, v} becomes two oppositely directed edges(u, v) and(v, u).
Here is an example of a small graph, drawn usingthis file using thecirco program from theGraphViz library:
Here is a similar directed graph, drawn usingthis file:
Given an edge(u, v), the verticesu andv are said to beincident to the edge andadjacent to each other. The number of vertices adjacent to a given vertexu is thedegree ofu; this can be divided into theout-degree (number of verticesv such that(u, v) is an edge) and thein-degree (number of verticesv such that(v, u) is an edge). A vertexv adjacent tou is called aneighbor ofu, and (in a directed graph) is apredecessor ofu if(v, u) is an edge and asuccessor ofu if(u, v) is an edge. We will allow a node to be its own predecessor and successor.
Graphs can be used to model any situation where we have things that are related to each other in pairs; for example, all of the following can be represented by graphs:
What would we like to do to graphs? Generally, we first have to build a graph by starting with a set of nodes and adding in any edges we need, and then we want to extract information from it, such as “Is this graph connected?”, “What is the shortest path in this graph froms tot?”, or “How many edges can I remove from this graph before some nodes become unreachable from other nodes?” There are standard algorithms for answering all of these questions; the information these algorithms need is typically (a) given a vertexu, what successors does it have; and sometimes (b) given verticesu andv, does the edge(u, v) exist in the graph?
A good graph representation will allow us to answer one or both of these questions quickly. There are generally two standard representations of graphs that are used in graph algorithms, depending on which question is more important.
For both representations, we simplify the representation task by insisting that vertices be labeled0, 1, 2, …, n − 1, wheren is the number of vertices in the graph. If we have a graph with different vertex labels (say, airport codes), we can enforce an integer labeling by a preprocessing step where we assign integer labels, and then translate the integer labels back into more useful user labels afterwards. The preprocessing step can usually be done using ahash table inO(n) time, which is likely to be smaller than the cost of whatever algorithm we are running on our graph, and the savings in code complexity and running time from working with just integer labels will pay this cost back many times over.
Anadjacency matrix is just a matrixa wherea[i][j] is1 if (i,j) is an edge in the graph and0 otherwise. It’s easy to build an adjacency matrix, and adding or testing for the existence of an edges takesO(1) time. The downsides of adjacency matrices are that finding all the outgoing edges from a vertex takesO(n) time even if there aren’t very many, and theO(n2) space cost is high for “sparse graphs,” those with much fewer thann2 edges.
Anadjacency list representation of a graph creates a list of successors for each nodeu. These lists may be represented as linked lists (the typical assumption in algorithms textbooks), or in languages like C may be represented by variable-length arrays. The cost for adding an edge is stillO(1), but testing for the existence of an edge(u, v) rises toO(d+(u)), whered+(u) is the out-degree ofu (i.e., the length of the list ofu’s successors). The cost of enumerating the successors ofu is alsoO(d+(u)), which is clearly the best possible since it takes that long just to write them all down. Finding predecessors of a nodeu is extremely expensive, requiring looking through every list of every node in timeO(n + m), wherem is the total number of edges, although if this is something we actually need to do often we can store a second copy of the graph with the edges reversed.
Adjacency lists are thus most useful when we mostly want to enumerate outgoing edges of each node. This is common in search tasks, where we want to find a path from one node to another or compute the distances between pairs of nodes. If other operations are important, we can optimize them by augmenting the adjacency list representation; for example, using sorted arrays for the adjacency lists reduces the cost of edge existence testing toO(log (d+(u))), and adding a second copy of the graph with reversed edges lets us find all predecessors of u inO(d−(u)) time, whered−(u) isu’s in-degree.
Adjacency lists also require much less space than adjacency matrices for sparse graphs:O(n + m) vsO(n2) for adjacency matrices. For this reason adjacency lists are more commonly used than adjacency matrices.
Here is an implementation of a basic graph type using adjacency lists.
/* basic directed graph type */typedefstruct graph *Graph;/* create a new graph with n vertices labeled 0..n-1 and no edges */Graph graphCreate(int n);/* free all space used by graph */void graphDestroy(Graph);/* add an edge to an existing graph *//* doing this more than once may have unpredictable results */void graphAddEdge(Graph,int source,int sink);/* return the number of vertices/edges in the graph */int graphVertexCount(Graph);int graphEdgeCount(Graph);/* return the out-degree of a vertex */int graphOutDegree(Graph,int source);/* return 1 if edge (source, sink) exists), 0 otherwise */int graphHasEdge(Graph,int source,int sink);/* invoke f on all edges (u,v) with source u *//* supplying data as final parameter to f *//* no particular order is guaranteed */void graphForeach(Graph g,int source,void (*f)(Graph g,int source,int sink,void *data),void *data);#include<stdlib.h>#include<assert.h>#include"graph.h"/* basic directed graph type *//* the implementation uses adjacency lists * represented as variable-length arrays *//* these arrays may or may not be sorted: if one gets long enough * and you call graphHasEdge on its source, it will be */struct graph {int n;/* number of vertices */int m;/* number of edges */struct successors {int d;/* number of successors */int len;/* number of slots in array */int isSorted;/* true if list is already sorted */int list[];/* actual list of successors starts here */ } *alist[];};/* create a new graph with n vertices labeled 0..n-1 and no edges */GraphgraphCreate(int n){ Graph g;int i; g = malloc(sizeof(struct graph) +sizeof(struct successors *) * n); assert(g); g->n = n; g->m =0;for(i =0; i < n; i++) { g->alist[i] = malloc(sizeof(struct successors)); assert(g->alist[i]); g->alist[i]->d =0; g->alist[i]->len =0; g->alist[i]->isSorted=1; }return g;}/* free all space used by graph */voidgraphDestroy(Graph g){int i;for(i =0; i < g->n; i++) free(g->alist[i]); free(g);}/* add an edge to an existing graph */voidgraphAddEdge(Graph g,int u,int v){ assert(u >=0); assert(u < g->n); assert(v >=0); assert(v < g->n);/* do we need to grow the list? */while(g->alist[u]->d >= g->alist[u]->len) { g->alist[u]->len = g->alist[u]->len *2 +1;/* +1 because it might have been 0 */ g->alist[u] = realloc(g->alist[u],sizeof(struct successors) +sizeof(int) * g->alist[u]->len); }/* now add the new sink */ g->alist[u]->list[g->alist[u]->d++] = v; g->alist[u]->isSorted =0;/* bump edge count */ g->m++;}/* return the number of vertices in the graph */intgraphVertexCount(Graph g){return g->n;}/* return the number of vertices in the graph */intgraphEdgeCount(Graph g){return g->m;}/* return the out-degree of a vertex */intgraphOutDegree(Graph g,int source){ assert(source >=0); assert(source < g->n);return g->alist[source]->d;}/* when we are willing to call bsearch */#define BSEARCH_THRESHOLD (10)staticintintcmp(constvoid *a,constvoid *b){return *((constint *) a) - *((constint *) b);}/* return 1 if edge (source, sink) exists), 0 otherwise */intgraphHasEdge(Graph g,int source,int sink){int i; assert(source >=0); assert(source < g->n); assert(sink >=0); assert(sink < g->n);if(graphOutDegree(g, source) >= BSEARCH_THRESHOLD) {/* make sure it is sorted */if(! g->alist[source]->isSorted) { qsort(g->alist[source]->list, g->alist[source]->d,sizeof(int), intcmp); }/* call bsearch to do binary search for us */return bsearch(&sink, g->alist[source]->list, g->alist[source]->d,sizeof(int), intcmp) !=0; }else {/* just do a simple linear search *//* we could call lfind for this, but why bother? */for(i =0; i < g->alist[source]->d; i++) {if(g->alist[source]->list[i] == sink)return1; }/* else */return0; }}/* invoke f on all edges (u,v) with source u *//* supplying data as final parameter to f */voidgraphForeach(Graph g,int source,void (*f)(Graph g,int source,int sink,void *data),void *data){int i; assert(source >=0); assert(source < g->n);for(i =0; i < g->alist[source]->d; i++) { f(g, source, g->alist[source]->list[i], data); }}And here is some test code:graphTest.c.
For some graphs, it may not make sense to represent them explicitly. An example might be the word-search graph fromCS223/2005/Assignments/HW10, which consists of all words in a dictionary with an edge between any two words that differ only by one letter. In such a case, rather than building an explicit data structure containing all the edges, we might generate edges as needed when computing the neighbors of a particular vertex. This gives us an implicit or procedural representation of a graph.
Implicit representations require the ability to return a vector or list of values from the neighborhood-computing function. There are various way to do this, of which the most sophisticated might be to use aniterator.
Apath is a sequence of verticesv1, v2, …vk where each pair(vi, vi + 1) is an edge. Often we want to find a path from a source vertexs to a target vertext, or more generally to detect which vertices are reachable from a given source vertexs. We can solve these problems by using any of several standard graph search algorithms, of which the simplest and most commonly used aredepth-first search andbreadth-first search.
Both of these search algorithms are a special case of a more general algorithm for growing a directed tree in a graph rooted at a given nodes. Here we are usingtree as a graph theorist would, to mean any set ofk nodes joined byk − 1 edges. This is similar to trees used in data structures except that there are no limits on the number of children a node can have and no ordering constraints within the tree.
The general tree-growing algorithm might be described as follows:
Practically, steps 2 and 3 are implemented by having some sort of data structure that acts as a bucket for unprocessed edges. When a new node is added to the tree, all of its outgoing edges are thrown into the bucket. The “best” outgoing edge is obtained by applying some sort of pop, dequeue, or delete-min operation to the bucket, depending on which it provides; if this edge turns out to be an internal edge of the tree (maybe we added its sink after putting it in the bucket), we throw it away. Otherwise we mark the edge and its sink as belonging to the tree and repeat.
The output of the generic tree-growing algorithm typically consists of (a) marks on all the nodes that are reachable froms, and (b) for each such nodev, a parent pointer back to the source of the edge that broughtv into the tree. Often these two values can be combined by using a null parent pointer to represent the absence of a mark (this usually requires making the root point to itself so that we know it’s in the tree). Other values that may be useful are a table showing the order in which nodes were added to the tree.
What kind of tree we get depends on what we use for the bucket—specifically, on what edge is returned when we ask for the “best” edge. Two easy cases are:
Structurally, these algorithms are almost completely identical; indeed, if we organize the stack/queue so that it can pop from both ends, we can switch between depth-first search and breadth-first search just by choosing which end to pop from.
Below, we give acombined implementation of both depth-first search and breadth-first search that does precisely this, although this is mostly for show. Typical implementations of breadth-first search include a further optimization, where we test an edge to see if we should add it to the tree (and possibly add it) before inserting into the queue. This gives the same result as the DFS-like implementation but only requiresO(n) space for the queue instead ofO(m), with a smaller constant as well since don’t need to bother storing source edges in the queue. An example of this approach is givenbelow.
The running time of any of these algorithms isvery fast: we payO(1) per vertex in setup costs andO(1) per edge during the search (assuming the input is in adjacency-list form), giving a linearO(n + m) total cost. Often it is more expensive to set up the graph in the first place than to run a search on it.
Here is a simple implementation of depth-first search, using a recursive algorithm, and breadth-first search, using an iterative algorithm that maintains a queue of vertices. In both cases the algorithm is applied to a sample graph whose vertices are the integers0 throughn − 1 for somen, and in which vertexx has edges to verticesx/2,3 ⋅ x, andx + 1, whenever these values are also integers in the range0 throughn − 1. For large graphs it may be safer to run aniterative version of DFS that uses an explicit stack instead of a possibly very deep recursion.
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<stdint.h>typedefint Vertex;#define VERTEX_NULL (-1)struct node { Vertex *neighbors;/* array of outgoing edges, terminated by VERTEX_NULL */ Vertex parent;/* for search */};struct graph {size_t n;/* number of vertices */struct node *v;/* list of vertices */};voidgraphDestroy(struct graph *g){ Vertex v;for(v =0; v < g->n; v++) { free(g->v[v].neighbors); } free(g);}/* this graph has edges from x to x+1, x to 3*x, and x to x/2 (when x is even) */struct graph *makeSampleGraph(size_t n){struct graph *g; Vertex v;constint allocNeighbors =4;int i; g = malloc(sizeof(*g)); assert(g); g->n = n; g->v = malloc(sizeof(struct node) * n); assert(g->v);for(v =0; v < n; v++) { g->v[v].parent = VERTEX_NULL;/* fill in neighbors */ g->v[v].neighbors = malloc(sizeof(Vertex) * allocNeighbors); i =0;if(v %2 ==0) { g->v[v].neighbors[i++] = v/2; }if(3*v < n) { g->v[v].neighbors[i++] =3*v; }if(v+1 < n) { g->v[v].neighbors[i++] = v+1; } g->v[v].neighbors[i++] = VERTEX_NULL; }return g;}/* output graph in dot format */voidprintGraph(conststruct graph *g){ Vertex u;size_t i; puts("digraph G {");for(u =0; u < g->n; u++) {for(i =0; g->v[u].neighbors[i] != VERTEX_NULL; i++) { printf("%d -> %d;\n", u, g->v[u].neighbors[i]); } } puts("}");}/* reconstruct path back to root from u */voidprintPath(conststruct graph *g, Vertex u){do { printf(" %d", u); u = g->v[u].parent; }while(g->v[u].parent != u);}/* print the tree in dot format */voidprintTree(conststruct graph *g){ Vertex u; puts("digraph G {");for(u =0; u < g->n; u++) {if(g->v[u].parent != VERTEX_NULL) { printf("%d -> %d;\n", u, g->v[u].parent); } } puts("}");}/* compute DFS tree starting at root *//* this uses a recursive algorithm and will not work on large graphs! */staticvoiddfsHelper(struct graph *g, Vertex parent, Vertex child){int i; Vertex neighbor;if(g->v[child].parent == VERTEX_NULL) { g->v[child].parent = parent;for(i =0; (neighbor = g->v[child].neighbors[i]) != VERTEX_NULL; i++) { dfsHelper(g, child, neighbor); } }}voiddfs(struct graph *g, Vertex root){ dfsHelper(g, root, root);}/* compute BFS tree starting at root */voidbfs(struct graph *g, Vertex root){ Vertex *q;int head;/* deq from here */int tail;/* enq from here */ Vertex current; Vertex nbr;int i; q = malloc(sizeof(Vertex) * g->n); assert(q); head = tail =0;/* push root onto q */ g->v[root].parent = root; q[tail++] = root;while(head < tail) { current = q[head++];for(i =0; (nbr = g->v[current].neighbors[i]) != VERTEX_NULL; i++) {if(g->v[nbr].parent == VERTEX_NULL) {/* haven't seen this guy *//* push it */ g->v[nbr].parent = current; q[tail++] = nbr; } } } free(q);}intmain(int argc,char **argv){int n;struct graph *g;if(argc !=3) { fprintf(stderr,"Usage: %s action n\nwhere action =\n g - print graph\n d - print dfs tree\n b - print bfs tree\n", argv[0]);return1; } n = atoi(argv[2]); g = makeSampleGraph(n);switch(argv[1][0]) {case'g': printGraph(g);break;case'd': dfs(g,0); printTree(g);break;case'b': bfs(g,0); printTree(g);break;default: fprintf(stderr,"%s: unknown action '%c'\n", argv[0], argv[1][0]);return1; } graphDestroy(g);return0;}The output of the program is either the graph, a DFS tree of the graph rooted at0, or a BFS tree of the graph rooted at0, in a format suitable for feeding to theGraphViz programdot, which draws pictures of graphs.
Here are the pictures forn = 20.
These are some older implementations of BFS and DFS that demonstrate how both can be written using the same code just by changing the behavior of the core data structure. This also demonstrates how to construct DFS iteratively; for BFS, thepreceding implementation is better in every respect.
/* Typical usage: * * struct searchInfo *s; * int n; * * s = searchInfoCreate(g); * * n = graph_vertices(g); * for(i = 0; i < n; i++) { * dfs(s, i); * } * * ... use results in s ... * * searchInfoDestroy(s); * *//* summary information per node for dfs and bfs *//* this is not intended to be opaque---user can read it *//* (but should not write it!) */#define SEARCH_INFO_NULL (-1)/* for empty slots */struct searchInfo { Graph graph;int reached;/* count of reached nodes */int *preorder;/* list of nodes in order first reached */int *time;/* time[i] == position of node i in preorder list */int *parent;/* parent in DFS or BFS forest */int *depth;/* distance from root */};/* allocate and initialize search results structure *//* you need to do this before passing it to dfs or bfs */struct searchInfo *searchInfoCreate(Graph g);/* free searchInfo data---does NOT free graph pointer */void searchInfoDestroy(struct searchInfo *);/* perform depth-first search starting at root, updating results */void dfs(struct searchInfo *results,int root);/* perform breadth-first search starting at root, updating results */void bfs(struct searchInfo *results,int root);#include<stdlib.h>#include<assert.h>#include"graph.h"#include"genericSearch.h"/* create an array of n ints initialized to SEARCH_INFO_NULL */staticint *createEmptyArray(int n){int *a;int i; a = malloc(sizeof(*a) * n); assert(a);for(i =0; i < n; i++) { a[i] = SEARCH_INFO_NULL; }return a;}/* allocate and initialize search results structure *//* you need to do this before passing it to dfs or bfs */struct searchInfo *searchInfoCreate(Graph g){struct searchInfo *s;int n; s = malloc(sizeof(*s)); assert(s); s->graph = g; s->reached =0; n = graphVertexCount(g); s->preorder = createEmptyArray(n); s->time = createEmptyArray(n); s->parent = createEmptyArray(n); s->depth = createEmptyArray(n);return s;}/* free searchInfo data---does NOT free graph pointer */voidsearchInfoDestroy(struct searchInfo *s){ free(s->depth); free(s->parent); free(s->time); free(s->preorder); free(s);}/* used inside search routines */struct edge {int u;/* source */int v;/* sink */};/* stack/queue */struct queue {struct edge *e;int bottom;int top;};staticvoidpushEdge(Graph g,int u,int v,void *data){struct queue *q; q = data; assert(q->top < graphEdgeCount(g) +1); q->e[q->top].u = u; q->e[q->top].v = v; q->top++;}/* this rather horrible function implements dfs if useQueue == 0 *//* and bfs if useQueue == 1 */staticvoidgenericSearch(struct searchInfo *r,int root,int useQueue){/* queue/stack */struct queue q;/* edge we are working on */struct edge cur;/* start with empty q *//* we need one space per edge *//* plus one for the fake (root, root) edge */ q.e = malloc(sizeof(*q.e) * (graphEdgeCount(r->graph) +1)); assert(q.e); q.bottom = q.top =0;/* push the root */ pushEdge(r->graph, root, root, &q);/* while q.e not empty */while(q.bottom < q.top) {if(useQueue) { cur = q.e[q.bottom++]; }else { cur = q.e[--q.top]; }/* did we visit sink already? */if(r->parent[cur.v] != SEARCH_INFO_NULL)continue;/* no */ assert(r->reached < graphVertexCount(r->graph)); r->parent[cur.v] = cur.u; r->time[cur.v] = r->reached; r->preorder[r->reached++] = cur.v;if(cur.u == cur.v) {/* we could avoid this if we were certain SEARCH_INFO_NULL *//* would never be anything but -1 */ r->depth[cur.v] =0; }else { r->depth[cur.v] = r->depth[cur.u] +1; }/* push all outgoing edges */ graphForeach(r->graph, cur.v, pushEdge, &q); } free(q.e);}voiddfs(struct searchInfo *results,int root){ genericSearch(results, root,0);}voidbfs(struct searchInfo *results,int root){ genericSearch(results, root,1);}And here is some test code:genericSearchTest.c. You will need to compilegenericSearchTest.c together with bothgenericSearch.c andgraph.c to get it to work. ThisMakefile will do this for you.
Stacks and queues are not the only options for the bucket in the generic search algorithm. Some other choices are:
Dynamic programming is a general-purposealgorithm design technique that is most often used to solvecombinatorial optimization problems, where we are looking for the best possible input to some function chosen from an exponentially large search space.
There are two parts to dynamic programming. The first part is a programming technique: dynamic programming is essentiallydivide and conquer run in reverse: we solve a big instance of a problem by breaking it up recursively into smaller instances; but instead of carrying out the computation recursively from the top down, we start from the bottom with the smallest instances of the problem, solving each increasingly large instance in turn and storing the result in a table. The second part is a design principle: in building up our table, we are careful always to preserve alternative solutions we may need later, by delaying commitment to particular choices to the extent that we can.
The bottom-up aspect of dynamic programming is most useful when a straightforward recursion would produce many duplicate subproblems. It is most efficient when we can enumerate a class of subproblems that doesn’t include too many extraneous cases that we don’t need for our original problem.
To take a simple example, suppose that we want to compute then-th Fibonacci number using the defining recurrence
A naive approach would simply code the recurrence up directly:
The running time of this procedure is easy to compute. The recurrence is
whose solution isΘ(an) wherea is the golden ratio1.6180339887498948482…. This is badly exponential.23
The problem is that we keep recomputing values offib that we’ve already computed. We can avoid this bymemoization, where we wrap our recursive solution in amemoizer that stores previously-computed solutions in ahash table. Sensible programming languages will let you write a memoizer once and apply it to arbitrary recursive functions. In less sensible programming languages it is usually easier just to embed the memoization in the function definition itself, like this:
intmemoFib(int n){int ret;if(hashContains(FibHash, n)) {return hashGet(FibHash, n); }else { ret = memoFib(n-1) + memoFib(n-2); hashPut(FibHash, n, ret);return ret; }}The assumption here is thatFibHash is a global hash table that we have initialized to map0 and1 to1. The total cost of running this procedure isO(n), becausefib is called at most twice for each valuek in0…n.
Memoization is a very useful technique in practice, but it is not popular with algorithm designers because computing the running time of a complex memoized procedure is often much more difficult than computing the time to fill a nice clean table. The use of a hash table instead of an array may also add overhead (and code complexity) that comes out in the constant factors. But it is always the case that a memoized recursive procedure considers no more subproblems than a table-based solution, and it may consider many fewer if we are sloppy about what we put in our table (perhaps because we can’t easily predict what subproblems will be useful).
Dynamic programming comes to the rescue. Because we know what smaller cases we have to reduce F(n) to, instead of computing F(n) top-down, we compute it bottom-up, hitting all possible smaller cases and storing the results in an array:
intfib2(int n){int *a;int i;int ret;if(n <2) {return1; }else { a = malloc(sizeof(*a) * (n+1)); assert(a); a[1] = a[2] =1;for(i =3; i <= n; i++) { a[i] = a[i-1] + a[i-2]; } } ret = a[n]; free(a);return ret;}Notice the recurrence is exactly the same in this version as in our original recursive version, except that instead of computing F(n-1) and F(n-2) recursively, we just pull them out of the array. This is typical of dynamic-programming solutions: often the most tedious editing step in converting a recursive algorithm to dynamic programming is changing parentheses to square brackets. As with memoization, the effect of this conversion is dramatic; what used to be an exponential-time algorithm is now linear-time.
Suppose that we want to compute thelongest increasing subsequence of an array. This is a sequence, not necessarily contiguous, of elements from the array such that each is strictly larger than the one before it. Since there are2n different subsequences of ann-element array, the brute-force approach of trying all of them might take a while.
What makes this problem suitable for dynamic programming is that any prefix of a longest increasing subsequence is a longest increasing subsequence of the part of the array that ends where the prefix ends; if it weren’t, we could make the big sequence longer by choosing a longer prefix. So to find the longest increasing subsequence of the whole array, we build up a table of longest increasing subsequences for each initial prefix of the array. At each step, when finding the longest increasing subsequence of elements0…i, we can just scan through all the possible values for the second-to-last element and read the length of the best possible subsequence ending there out of the table. When the table is complete, we can scan for the best last element and then work backwards to reconstruct the actual subsequence.
This last step requires some explanation. We don’t really want to store intable[i] the full longest increasing subsequence ending at positioni, because it may be very big. Instead, we store the index of the second-to-last element of this sequence. Since that second-to-last element also has a table entry that stores the index of its predecessor, by following the indices we can generate a subsequence of lengthO(n), even though we only storedO(1) pieces of information in each table entry. This is similar to the parent pointer technique used ingraph search algorithms.
Here’s what the code looks like:
/* compute a longest strictly increasing subsequence of an array of ints *//* input is array a with given length n *//* returns length of LIS *//* If the output pointer is non-null, writes LIS to output pointer. *//* Caller should provide at least sizeof(int)*n space for output *//* If there are multiple LIS's, which one is returned is arbitrary. */unsignedlonglongest_increasing_subsequence(constint a[],unsignedlong n,int *output);#include<stdlib.h>#include<assert.h>#include"lis.h"unsignedlonglongest_increasing_subsequence(constint a[],unsignedlong n,int *output){struct lis_data {unsignedlong length;/* length of LIS ending at this point */unsignedlong prev;/* previous entry in the LIS ending at this point */ } *table;unsignedlong best;/* best entry in table */unsignedlong scan;/* used to generate output */unsignedlong i;unsignedlong j;unsignedlong best_length;/* special case for empty table */if(n ==0)return0; table = malloc(sizeof(*table) * n);for(i =0; i < n; i++) {/* default best is just this element by itself */ table[i].length =1; table[i].prev = n;/* default end-of-list value *//* but try all other possibilities */for(j =0; j < i; j++) {if(a[j] < a[i] && table[j].length +1 > table[i].length) {/* we have a winner */ table[i].length = table[j].length +1; table[i].prev = j; } } }/* now find the best of the lot */ best =0;for(i =1; i < n; i++) {if(table[i].length > table[best].length) { best = i; } }/* table[best].length is now our return value *//* save it so that we don't lose it when we free table */ best_length = table[best].length;/* do we really have to compute the output? */if(output) {/* yes :-( */ scan = best;for(i =0; i < best_length; i++) { assert(scan >=0); assert(scan < n); output[best_length - i -1] = a[scan]; scan = table[scan].prev; } } free(table);return best_length;}A sample program that runslongest_increasing_subsequence on a list of numbers passed in bystdin is given intest_lis.c. Here is aMakefile.
Implemented like this, the cost of finding an LIS isO(n2), because to compute each entry in the array, we have to search through all the previous entries to find the longest path that ends at a value less than the current one. This can be improved by using a more clever data structure. If we use a binary search tree that stores path keyed by the last value, and augment each node with a field that represents the maximum length of any path in the subtree under that node, then we can find the longest feasible path that we can append the current node to inO(log n) time instead ofO(n) time. This brings the total cost down to onlyO(nlog n).
Suppose we want to compute the distance between any two points in a graph, where each edgeuv has a lengthℓuv ( + ∞ for edges not in the graph) and the distance between two verticess and t$ is the minimum over alls–t paths of the total length of the edges. There are various algorithms for doing this for a particulars andt, but there is also a very simple dynamic programming algorithm known asFloyd-Warshall that computes the distance between alln2 pairs of vertices inΘ(n3) time.
The assumption is that the graph does not contain anegative cycle (a cycle with total edge weight less than zero), so that for two connected nodes there is always a shortest path that uses each intermediate vertex at most once. If a graph does contain a negative cycle, the algorithm will detect it by reporting the distance fromi toi less than zero for somei.
Negative cycles don’t generally exist in distance graphs (unless you have the ability to move faster than the speed of light), but they can come up in other contexts. One example would be in currency arbitrage, where each node is some currency, the weight of an edgeuv is the logarithm of the exchange rate fromu tov, and the total weight of a path froms tot gives the logarithm of the number of units oft you can get for one unit ofs, since adding the logs along the path corresponds to multiplying all the exchange rates. In this context a negative cycle gives you a way to turn a dollar into less than a dollar by running it through various other currencies, which is not useful, but apositive cycle lets you pay for the supercomputer you bought to find it before anybody else did. If we negate all the edge weights, we turn a positive cycle into a negative cycle, making a fast algorithm for finding this negative cycle potentially valuable.
However, if we don’t have any negative cycles, the idea is that we can create restricted instances of the shortest-path problem by limiting the maximum index of any node used on the path. LetL(i, j, k) be the length of a shortest path fromi toj that uses only the vertices0, …, k − 1 along the path (not counting the endpointsi andj, which can be anything). Whenk = 0, this is just the length of thei–j edge, or + ∞ if there is no such edge. So we can start by computingL(i, j, 0) for alli. Now givenL(i, j, k) for alli and somek, we can computeL(i, j, k + 1) by observing that any shortesti–j path that has intermediate vertices in0…k either consists of a path with intermediate vertices in0…k − 1, or consists of a path fromi tok followed by a path fromk toj, where both of these paths have intermediate vertices in0…k − 1. So we get
Since this takesO(1) time to compute if we have previously computedL(i, j, k) for alli andj, we can fill in the entire table inO(n3) time.
Implementation details:
Given sequences of charactersv andw,v is asubsequence ofw if every character inv appears inw in the same order. For example,aaaaa,brac, andbadar are all subsequences ofabracadabra, butbadcar is not. A longest common subsequence (LCS for short) of two sequencesx andy is the longest sequence that is a subsequence of both: two longest common subsequences ofabracadabra andbadcar arebadar andbacar.
As with longest increasing subsequence, one can find the LCS of two sequence by brute force, but it will take even longer. Not only are there are2n subsequences of a sequence of lengthn, but checking each subsequence of the first to see if it is also a subsequence of the second may take some time. It is better to solve the problem using dynamic programming. Having sequences gives an obvious linear structure to exploit: the basic strategy will be to compute LCSs for increasingly long prefixes of the inputs. But with two sequences we will have to consider prefixes of both, which will give us a two-dimensional table where rows correspond to prefixes of sequencex and columns correspond to prefixes of sequencey.
The recursive decomposition that makes this technique work looks like this. LetL(x, y) be the length of the longest common subsequence ofx andy, wherex andy are strings. Leta andb be single characters. ThenL(xa, yb) is the maximum of:
The idea is that we either have a new matching character we couldn’t use before (the first case), or we have an LCS that doesn’t use one ofa orb (the remaining cases). In each case the recursive call to LCS involves a shorter prefix ofxa oryb, with an ultimate base caseL(x, y) = 0 if at least one ofx ory is the empty string. So we can fill in these values in a table, as long as we are careful to make sure that the shorter prefixes are always filled first. If we are smart about remembering which case applies at each step, we can even go back and extract an actual LCS, by stitching together to places wherea = b. Here’s a short C program that does this:
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<string.h>#include<limits.h>/* compute longest common subsequence of argv[1] and argv[2] *//* computes longest common subsequence of x and y, writes result to lcs *//* lcs should be pre-allocated by caller to 1 + minimum length of x or y */voidlongestCommonSubsequence(constchar *x,constchar *y,char *lcs){int xLen;int yLen;int i;/* position in x */int j;/* position in y */ xLen = strlen(x); yLen = strlen(y);/* best choice at each position *//* length gives length of LCS for these prefixes *//* prev points to previous substring *//* newChar if non-null is new character */struct choice {int length;struct choice *prev;char newChar; } best[xLen][yLen];for(i =0; i < xLen; i++) {for(j =0; j < yLen; j++) {/* we can always do no common substring */ best[i][j].length =0; best[i][j].prev =0; best[i][j].newChar =0;/* if we have a match, try adding new character *//* this is always better than the nothing we started with */if(x[i] == y[j]) { best[i][j].newChar = x[i];if(i >0 && j >0) { best[i][j].length = best[i-1][j-1].length +1; best[i][j].prev = &best[i-1][j-1]; }else { best[i][j].length =1; } }/* maybe we can do even better by ignoring a new character */if(i >0 && best[i-1][j].length > best[i][j].length) {/* throw away a character from x */ best[i][j].length = best[i-1][j].length; best[i][j].prev = &best[i-1][j]; best[i][j].newChar =0; }if(j >0 && best[i][j-1].length > best[i][j].length) {/* throw away a character from x */ best[i][j].length = best[i][j-1].length; best[i][j].prev = &best[i][j-1]; best[i][j].newChar =0; } } }/* reconstruct string working backwards from best[xLen-1][yLen-1] */int outPos;/* position in output string */struct choice *p;/* for chasing linked list */ outPos = best[xLen-1][yLen-1].length; lcs[outPos--] = '\0';for(p = &best[xLen-1][yLen-1]; p; p = p->prev) {if(p->newChar) { assert(outPos >=0); lcs[outPos--] = p->newChar; } }}intmain(int argc,char **argv){if(argc !=3) { fprintf(stderr,"Usage: %s string1 string2\n", argv[0]);return1; }char output[strlen(argv[1]) +1]; longestCommonSubsequence(argv[1], argv[2], output); printf("\"%s\" (%zu characters)\n", output, strlen(output));return0;}The whole thing takesO(nm) time wheren andm are the lengths ofA andB.
Randomization is a fundamental technique in algorithm design, that allows programs to run quickly when the average-case behavior of an algorithm is better than the worst-case behavior. It is also heavily used in games, both in entertainment and gambling. The latter application gives the only example I know of aprogrammer who died from writing bad code, which shows how serious good random-number generation is.
If you want random values in a C program, there are three typical ways of getting them, depending on how good (i.e. uniform, uncorrelated, and unpredictable) you want them to be.
rand function from the standard libraryE.g.
Therand function, declared instdlib.h, returns a random-looking integer in the range 0 toRAND_MAX (inclusive) every time you call it. On machines using the GNU C libraryRAND_MAX is equal toINT_MAX which is typically231 − 1, butRAND_MAX may be as small as 32767. There are no particularly strong guarantees about the quality of random numbers thatrand returns, but it should be good enough for casual use, and it has the advantage that as part of the C standard you can assume it is present almost everywhere.
Note thatrand is apseudorandom number generator: the sequence of values it returns is predictable if you know its starting state (and is still predictable from past values in the sequence even if you don’t know the starting state, if you are clever enough). It is also the case that the initial seed is fixed, so that the program above will print the same value every time you run it.
This is a feature: it permits debugging randomized programs. As John von Neumann, who proposed pseudorandom number generators in his 1946 talk “Various Techniques Used in Connection With Random Digits,” explained:
We see then that we could build a physical instrument to feed random digits directly into a high-speed computing machine and could have the control call for these numbers as needed. The real objection to this procedure is the practical need for checking computations. If we suspect that a calculation is wrong, almost any reasonable check involves repeating something done before. At that point the introduction of new random numbers would be intolerable.
srandIf you want to get different sequences, you need toseed the random number generator usingsrand. A typical use might be:
#include<stdio.h>#include<stdlib.h>#include<time.h>intmain(int argc,char **argv){ srand(time(0)); printf("%d\n", rand());return0;}Heretime(0) returns the number of seconds since the epoch (00:00:00 UTC, January 1, 1970, for POSIX systems, not counting leap seconds). Note that this still might give repeated values if you run it twice in the same second, and it’s extremely dangerous if you expect to distribute your code to a lot of people who want different results, since two of your usersare likely to run it twice in the same second. See the discussion of/dev/urandom below for a better method.
There has been quite a bit of research on pseudorandom number generators over the years, and much better pseudorandom number generators thanrand are available. Currently, the most widely used pseudorandom random number generator isMersenne Twister, which runs about 4 times faster thanrand in its standard C implementation and passes a much wider battery of statistical tests. Its English-language home page is athttp://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html. As withrand, you still need to provide an initial seed value.
There are alsocryptographically secure pseudorandom number generators, of which the most famous isBlum Blum Shub. These cannot be predicted based on their output if seeded with a true random value (under certain cryptographic assumptions: hardness of factoring for Blum Blum Shub). Unfortunately, cryptographic PRNGs are usually too slow for day-to-day use.
If you really need actual random numbers and are on a Linux or BSD-like operating system, you can use the special device files/dev/random and/dev/urandom. These can be opened for reading like ordinary files, but the values read from them are a random sequence of bytes (including null characters). A typical use might be:
#include<stdio.h>intmain(int argc,char **argv){unsignedint randval;FILE *f; f = fopen("/dev/random","r"); fread(&randval,sizeof(randval),1, f); fclose(f); printf("%u\n", randval);return0;}(A similar construction can also be used to obtain a better initial seed forsrand thantime(0).)
Both/dev/random and/dev/urandom derive their random bits from physically random properties of the computer, like time between keystrokes or small variations in hard disk rotation speeds. The difference between the two is that/dev/urandom will always give you some random-looking bits, even if it has to generate extra ones using a cryptographic pseudo-random number generator, while/dev/random will only give you bits that it is confident are in fact random. Since your computer only generates a small number of genuinely random bits per second, this may mean that/dev/random will exhaust its pool if read too often. In this case, a read on/dev/random will block (just like reading a terminal with no input on it) until the pool has filled up again.
Neither/dev/random nor/dev/urandom is known to be secure against a determined attacker, but they are about the best you can do without resorting to specialized hardware.
The problem withrand is that getting a uniform value between 0 and231 − 1 may not be what you want. It could be thatRAND_MAX is be too small; in this case, you may have to callrand more than once and paste together the results. But there can be problems withRAND_MAX even if it is bigger than the values you want.
For example, suppose you want to simulate a die roll for your video craps machine, but you don’t want to get whacked by Johnny “The Debugger” when the Nevada State Gaming Commission notices that 6-6 is coming up slightly less often than it’s supposed to. A natural thing to try would be to take the output ofrand mod 6:
The problem here is that there are231 outputs from rand, and 6 doesn’t divide231. So 1 and 2 are slightly more likely to come up than 3, 4, 5, or 6. This can be particularly noticeable if we want a uniform variable from a larger range, e.g.[0…⌊(2/3) ⋅ 231⌋].
We can avoid this with a technique calledrejection sampling, where we reject excess parts of the output range ofrand. For rolling a die, the trick is to reject anything in the last extra bit of the range that is left over after the largest multiple of the die size. Here’s a routine that does this, returning a uniform value in the range 0 to n-1 for any positive n, together with a program that demonstrates its use for rolling dice:
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<time.h>/* return a uniform random value in the range 0..n-1 inclusive */intrandRange(int n){int limit;int r; limit = RAND_MAX - (RAND_MAX % n);while((r = rand()) >= limit);return r % n;}intmain(int argc,char **argv){int i; srand(time(0));for(i =0; i <40; i++) { printf("%d ", randRange(6)+1); } putchar('\n');return0;}More generally, rejection sampling can be used to get random values with particular properties, where it’s hard to generate a value with that property directly. Here’s a program that generates random primes:
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<time.h>/* return 1 if n is prime */intisprime(int n){int i;if(n %2 ==0 || n ==1) {return0; }for(i =3; i*i <= n; i +=2) {if(n % i ==0) {return0; } }return1;}/* return a uniform random value in the range 0..n-1 inclusive */intrandPrime(void){int r;/* extra parens avoid warnings */while(!isprime((r = rand())));return r;}intmain(int argc,char **argv){int i; srand(time(0));for(i =0; i <10; i++) { printf("%d\n", randPrime()); }return0;}One temptation to avoid is to re-use your random values. If, for example, you try to find a random prime by picking a random x and trying x, x+1, x+2, etc., until you hit a prime, some primes are more likely to come up than others.
Randomized algorithms typically make random choices to get good average worst-case performance in situations where a similar deterministic algorithm would fail badly for some inputs but perform well on most inputs. The idea is that the randomization scrambles the input space so that the adversary can’t predict which possible input values will be bad for us. This still allows it to make trouble if it gets lucky, but most of the time our algorithm should run quickly.
This is essentially rejection sampling in disguise. Suppose that you want to find one of many needles in a large haystack. One approach is to methodically go through the straws/needles one at a time until you find a needle. But you may find that your good friend the adversary has put all the needles at the end of your list. Picking candidate at random is likely to hit a needle faster if there are many of them.
Here is a (silly) routine that quickly finds a number whose high-order bits match a particular pattern:
intmatchBits(int pattern){int r;while(((r = rand()) &0x70000000) != (pattern &0x70000000));return r;}This will find a winning value in 8 tries on average. In contrast, this deterministic version will take a lot longer for nonzero patterns:
intmatchBitsDeterministic(int pattern){int i;for(i =0; (i &0x70000000) != (pattern &0x70000000); i++);return i;}The downside of the randomized approach is that it’s hard to tell when to quit if there are no matches; if we stop after some fixed number of trials, we get aMonte Carlo algorithm that may give the wrong answer with small probability. The usual solution is to either accept a small probability of failure, or interleave a deterministic backup algorithm that always works. The latter approach gives aLas Vegas algorithm whose running time is variable but whose correctness is not.
Quickselect, orHoare’s FIND (Hoare, C. A. R. Algorithm 65: FIND, CACM 4(7):321–322, July 1961), is an algorithm for quickly finding thek-th largest element in an unsorted array ofn elements. It runs inO(n) time on average, which is the best one can hope for (we have to look at every element of the array to be sure we didn’t miss a small one that changes our answer) and better than theO(nlog n) time we get if we sort the array first using a comparison-based sorting algorithm.
The idea is to pick a random pivot and divide the input into two piles, each of which is likely to be roughly a constant fraction of the size of the original input.24 It takes O(n) time to split the input up (we have to compare each element to the pivot once), and in the recursive calls this gives a geometric series. We can even do the splitting up in place if we are willing to reorder the elements of our original array.
If we recurse into both piles instead of just one, we getquicksort (Hoare, C. A. R. Algorithm 64: Quicksort. CACM 4(7):321, July 1961), a very fast and simple comparison-based sorting algorithm. Here is an implementation of both algorithms:
#include<stdio.h>#include<stdlib.h>#include<assert.h>/* reorder an array to put elements <= pivot * before elements > pivot. * Returns number of elements <= pivot */staticintsplitByPivot(int n,int *a,int pivot){int lo;int hi;int temp;/* for swapping */ assert(n >=0);/* Dutch Flag algorithm *//* swap everything <= pivot to bottom of array *//* invariant is i < lo implies a[i] <= pivot *//* and i > hi implies a[i] > pivot */ lo =0; hi = n-1;while(lo <= hi) {if(a[lo] <= pivot) { lo++; }else { temp = a[hi]; a[hi--] = a[lo]; a[lo] = temp; } }return lo;}/* find the k-th smallest element of an n-element array *//* may reorder elements of the original array */intquickselectDestructive(int k,int n,int *a){int pivot;int lo; assert(0 <= k); assert(k < n);if(n ==1) {return a[0]; }/* else */ pivot = a[rand() % n];/* we will tolerate non-uniformity */ lo = splitByPivot(n, a, pivot);/* lo is now number of values <= pivot */if(k < lo) {return quickselectDestructive(k, lo, a); }else {return quickselectDestructive(k - lo, n - lo, a + lo); }}/* sort an array in place */voidquickSort(int n,int *a){int pivot;int lo;if(n <=1) {return; }/* else */ pivot = a[rand() % n];/* we will tolerate non-uniformity */ lo = splitByPivot(n, a, pivot); quickSort(lo, a); quickSort(n - lo, a + lo);}/* shuffle an array */voidshuffle(int n,int *a){int i;int r;int temp;for(i = n -1; i >0; i--) { r = rand() % i; temp = a[r]; a[r] = a[i]; a[i] = temp; }}#define N (1024)intmain(int argc,char **argv){int a[N];int i; srand(0);/* use fixed value for debugging */for(i =0; i < N; i++) { a[i] = i; } shuffle(N, a);for(i =0; i < N; i++) { assert(quickselectDestructive(i, N, a) == i); } shuffle(N, a); quickSort(N, a);for(i =0; i < N; i++) { assert(a[i] == i); }return0;}Suppose we insertn elements into an initially-empty binary search tree in random order with no rebalancing. Then each element is equally likely to be the root, and all the elements less than the root end up in the left subtree, while all the elements greater than the root end up in the right subtree, where they are further partitioned recursively. This is exactly what happens in quicksort, so the structure of the tree will exactly mirror the structure of an execution of quicksort. In particular, the average depth of a node will beO(log n), giving us the same expected search cost as in a balanced binary tree.
The problem with this approach is that we don’t have any guarantees that the input will be supplied in random order, and in the worst case we end up with a linked list. The solution is to put the randomization into the algorithm itself, making the structure of the tree depend on random choices made by the program itself.
Askip list (Pugh, 1990) is a randomized tree-like data structure based on linked lists. It consists of a level 0 list that is an ordinary sorted linked list, together with higher-level lists that contain a random sampling of the elements at lower levels. When inserted into the level i list, an element flips a coin that tells it with probability p to insert itself in the level i+1 list as well.
Searches in a skip list are done by starting in the highest-level list and searching forward for the last element whose key is smaller than the target; the search then continues in the same way on the next level down. The idea is that the higher-level lists act as express lanes to get us to our target value faster. To bound the expected running time of a search, it helps to look at this process backwards; the reversed search path starts at level 0 and continues going backwards until it reaches the first element that is also in a higher level; it then jumps to the next level up and repeats the process. On average, we hit1 + 1/p nodes at each level before jumping back up; for constantp (e.g.1/2), this gives usO(log n) steps for the search.
The space per element of a skip list also depends onp. Every element has at least one outgoing pointer (on level 0), and on average has exactly1/(1 − p) expected pointers. So the space cost can also be adjusted by adjustingp. For example, if space is at a premium, settingp = 1/10 produces10/9 pointers per node on average—not much more than in a linked list—but still givesO(log n) search time.
Below is an implementation of a skip list. To avoid having to allocate a separate array of pointers for each element, we put a length-1 array at the end ofstruct skiplist and rely on C’s lack of bounds checking to make the array longer if necessary. An empty head element stores pointers to all the initial elements in each level of the skip list; it is given the fake keyINT_MIN so that searches for values less than any in the list will report this value. Aside from these nasty tricks, the code for search and insertion is pretty straightforward. Code for deletion is a little more involved, because we have to make sure that we delete the leftmost copy of a key if there are duplicates (an alternative would be to modifyskiplistInsert to ignore duplicates).
#include<stdlib.h>#include<assert.h>#include<limits.h>#include"skiplist.h"#define MAX_HEIGHT (32)struct skiplist {int key;int height;/* number of next pointers */struct skiplist *next[1];/* first of many */};/* choose a height according to a geometric distribution */staticintchooseHeight(void){int i;for(i =1; i < MAX_HEIGHT && rand() %2 ==0; i++);return i;}/* create a skiplist node with the given key and height *//* does not fill in next pointers */static SkiplistskiplistCreateNode(int key,int height){ Skiplist s; assert(height >0); assert(height <= MAX_HEIGHT); s = malloc(sizeof(struct skiplist) +sizeof(struct skiplist *) * (height -1)); assert(s); s->key = key; s->height = height;return s;}/* create an empty skiplist */SkiplistskiplistCreate(void){ Skiplist s;int i;/* s is a dummy head element */ s = skiplistCreateNode(INT_MIN, MAX_HEIGHT);/* this tracks the maximum height of any node */ s->height =1;for(i =0; i < MAX_HEIGHT; i++) { s->next[i] =0; }return s;}/* free a skiplist */voidskiplistDestroy(Skiplist s){ Skiplist next;while(s) { next = s->next[0]; free(s); s = next; }}/* return maximum key less than or equal to key *//* or INT_MIN if there is none */intskiplistSearch(Skiplist s,int key){int level;for(level = s->height -1; level >=0; level--) {while(s->next[level] && s->next[level]->key <= key) { s = s->next[level]; } }return s->key;}/* insert a new key into s */voidskiplistInsert(Skiplist s,int key){int level; Skiplist elt; elt = skiplistCreateNode(key, chooseHeight()); assert(elt);if(elt->height > s->height) { s->height = elt->height; }/* search through levels taller than elt */for(level = s->height -1; level >= elt->height; level--) {while(s->next[level] && s->next[level]->key < key) { s = s->next[level]; } }/* now level is elt->height - 1, we can start inserting */for(; level >=0; level--) {while(s->next[level] && s->next[level]->key < key) { s = s->next[level]; }/* s is last entry on this level < new element *//* do list insert */ elt->next[level] = s->next[level]; s->next[level] = elt; }}/* delete a key from s */voidskiplistDelete(Skiplist s,int key){int level; Skiplist target;/* first we have to find leftmost instance of key */ target = s;for(level = s->height -1; level >=0; level--) {while(target->next[level] && target->next[level]->key < key) { target = target->next[level]; } }/* take one extra step at bottom */ target = target->next[0];if(target ==0 || target->key != key) {return; }/* now we found target, splice it out */for(level = s->height -1; level >=0; level--) {while(s->next[level] && s->next[level]->key < key) { s = s->next[level]; }if(s->next[level] == target) { s->next[level] = target->next[level]; } } free(target);}Here is the header file, Makefile, and test code:skiplist.h,Makefile,test_skiplist.c.
Randomization can also be useful in hash tables. Recall that in building a hash table, we are relying on the hash function to spread out bad input distributions over the indices of our array. But for any fixed hash function, in the worst case we may get inputs where every key hashes to the same location.Universal hashing (Carter and Wegman, 1979) solves this problem by choosing a hash function at random. We may still get unlucky and have the hash function hash all our values to the same location, but now we are relying on the random number generator to be nice to us instead of the adversary. We can also rehash with a new random hash function if we find out that the one we are using is bad.
The problem here is that we can’t just choose a function uniformly at random out of the set of all possible hash functions, because there are too many of them, meaning that we would spend more space representing our hash function than we would on the table. The solution is to observe that we don’t need our hash function h to be truly random; it’s enough if the probability of collision Pr[h(x) = h(y)] for any fixed keysx ≠ y is1/m, wherem is the size of the hash table. The reason is that the cost of searching for x (with chaining) is linear in the number of keys already in the table that collide with it. The expected number of such collisions is the sum of Pr[h(x) = h(y)] over all keys y in the table, or n/m if we have n keys. So this pairwise collision probability bound is enough to get the desired n/m behavior out of our table. A family of hash function with this property is calleduniversal.
How do we get a universal hash family? For strings, we can use a table of random values, one for each position and possible character in the string. The hash of a string is then the exclusive or of the random valueshashArray[i][s[i]] corresponding to the actual characters in the string. If our table has size a power of two, this has the universal property, because if two strings x and y differ in some position i, then there is only one possible value ofhashArray[i][y[i]] (mod m) that will make the hash functions equal.
Typically to avoid having to build an arbitrarily huge table of random values, we only has an initial prefix of the string. Here is a hash function based on this idea, which assumes that thed data structure includes ahashArray field that contains the random values for this particular hash table:
staticunsignedlonghash_function(Dict d,constchar *s){unsignedconstchar *us;unsignedlong h;int i; h =0; us = (unsignedconstchar *) s;for(i =0; i < HASH_PREFIX_LENGTH && us[i] != '\0'; i++) { h ^= d->hashArray[i][us[i]]; }return h;}A modified version of theDict hash table from thechapter on hash tables that uses this hash function is given here:dict.c,dict.h,test_dict.c,Makefile.
Most of the time, when we’ve talked about the asymptotic performance of data structures, we have implicitly assumed that the keys we are looking up are of constant size. This means that computing a hash function or comparing two keys (as in a binary search tree) takesO(1) time. What if this is not the case?
If we consider anm-character string, any reasonable hash function is going to takeO(m) time since it has to look at all of the characters. Similarly, comparing twom-character strings may also takeO(m) time. If we charge for this (as we should!) then the cost of hash table operations goes fromO(1) toO(m) and the cost of binary search tree operations, even in a balanced tree, goes fromO(log n) toO(mlog n). Even sorting becomes more expensive: a sorting algorithm that doesO(nlog n) comparisons may now takeO(mnlog n) time. But maybe we can exploit the structure of strings to get better performance.
Radix search refers to a variety of data structures that support searching for strings considered as sequences of digits in some large base (orradix). These are generally faster than simplebinary search trees because they usually only require examining one byte or less of the target string at each level of the tree, as compared to every byte in the target in a full string comparison. In many cases, the best radix search trees are even faster thanhash tables, because they only need to look at a small part of the target string to identify it.
We’ll describe several radix search trees, starting with the simplest and working up.
Atrie is a binary tree (or more generally, ak-ary tree wherek is the radix) where the root represents the empty bit sequence and the two children of a node representing sequencex represent the extended sequencesx0 andx1 (or generallyx0, x1, …, x(k − 1)). So a key is not stored at a particular node but is instead represented bit-by-bit (or digit-by-digit) along some path. Typically a trie assumes that the set of keys is prefix-free, i.e. that no key is a prefix of another; in this case there is a one-to-one correspondence between keys and leaves of the trie. If this is not the case, we can mark internal nodes that also correspond to the ends of keys, getting a slightly different data structure known as adigital search tree. For null-terminated strings as in C, the null terminator ensures that any set of strings is prefix-free.
Given this simple description, a trie storing a single long key would have a very large number of nodes. A standard optimization is to chop off any path with no branches in it, so that each leaf corresponds to the shortest unique prefix of a key. This requires storing the key in the leaf so that we can distinguish different keys with the same prefix.
The nametrie comes from the phrase “information retrieval.” Despite the etymology,trie is now almost always pronounced liketry instead oftree to avoid confusion with other tree data structures.
Searching a trie is similar to searching a binary search tree, except that instead of doing a comparison at each step we just look at the next bit in the target. The time to perform a search is proportional to the number of bits in the longest path in the tree matching a prefix of the target. This can be very fast for search misses if the target is wildly different from all the keys in the tree.
Insertion is more complicated for tries than for binary search trees. The reason is that a new element may add more than one new node. There are essentially two cases:
In either case, the cost is bounded by the length of the new key, which is about the best we can hope for in the worst case for any data structure.
A typical trie implementation in C might look like this. It uses aGET_BIT macro similar to the one from thechapter on bit manipulation, except that we reverse the bits within each byte to get the right sorting order for keys.
typedefstruct trie_node *Trie;#define EMPTY_TRIE (0)/* returns 1 if trie contains target */int trie_contains(Trie trie,constchar *target);/* add a new key to a trie *//* and return the new trie */Trie trie_insert(Trie trie,constchar *key);/* free a trie */void trie_destroy(Trie);/* debugging utility: print all keys in trie */void trie_print(Trie);#include<stdio.h>#include<stdlib.h>#include<string.h>#include<assert.h>#include"trie.h"#define BITS_PER_BYTE (8)/* extract the n-th bit of x *//* here we process bits within bytes in MSB-first order *//* this sorts like strcmp */#define GET_BIT(x, n) ((((x)[(n) / BITS_PER_BYTE]) & (0x1 << (BITS_PER_BYTE - 1 - (n) % BITS_PER_BYTE))) != 0)#define TRIE_BASE (2)struct trie_node {char *key;struct trie_node *kids[TRIE_BASE];};#define IsLeaf(t) ((t)->kids[0] == 0 && (t)->kids[1] == 0)/* returns 1 if trie contains target */inttrie_contains(Trie trie,constchar *target){int bit;for(bit =0; trie && !IsLeaf(trie); bit++) {/* keep going */ trie = trie->kids[GET_BIT(target, bit)]; }if(trie ==0) {/* we lost */return0; }else {/* check that leaf really contains the target */return !strcmp(trie->key, target); }}/* gcc -pedantic kills strdup! */staticchar *my_strdup(constchar *s){char *s2; s2 = malloc(strlen(s) +1); assert(s2); strcpy(s2, s);return s2;}/* helper functions for insert */static Triemake_trie_node(constchar *key){ Trie t;int i; t = malloc(sizeof(*t)); assert(t);if(key) { t->key = my_strdup(key); assert(t->key); }else { t->key =0; }for(i =0; i < TRIE_BASE; i++) t->kids[i] =0;return t;}/* add a new key to a trie *//* and return the new trie */Trietrie_insert(Trie trie,constchar *key){int bit;int bitvalue; Trie t; Trie kid;constchar *oldkey;if(trie ==0) {return make_trie_node(key); }/* else *//* first we'll search for key */for(t = trie, bit =0; !IsLeaf(t); bit++, t = kid) { kid = t->kids[bitvalue = GET_BIT(key, bit)];if(kid ==0) {/* woohoo! easy case */ t->kids[bitvalue] = make_trie_node(key);return trie; } }/* did we get lucky? */if(!strcmp(t->key, key)) {/* nothing to do */return trie; }/* else *//* hard case---have to extend the trie */ oldkey = t->key;#ifdef EXCESSIVE_TIDINESS t->key =0;/* not required but makes data structure look tidier */#endif/* walk the common prefix */while(GET_BIT(oldkey, bit) == (bitvalue = GET_BIT(key, bit))) { kid = make_trie_node(0); t->kids[bitvalue] = kid; bit++; t = kid; }/* then split */ t->kids[bitvalue] = make_trie_node(key); t->kids[!bitvalue] = make_trie_node(oldkey);return trie;}/* kill it */voidtrie_destroy(Trie trie){int i;if(trie) {for(i =0; i < TRIE_BASE; i++) { trie_destroy(trie->kids[i]); }if(IsLeaf(trie)) { free(trie->key); } free(trie); }}staticvoidtrie_print_internal(Trie t,int bit){int i;int kid;if(t !=0) {if(IsLeaf(t)) {for(i =0; i < bit; i++) putchar(' '); puts(t->key); }else {for(kid =0; kid < TRIE_BASE; kid++) { trie_print_internal(t->kids[kid], bit+1); } } }}voidtrie_print(Trie t){ trie_print_internal(t,0);}Here is a short test program that demonstrates how to use it:
#include<stdio.h>#include<stdlib.h>#include"trie.h"/* test for trie.c *//* reads lines from stdin and echoes lines that haven't appeared before *//* read a line of text from stdin * and return it (without terminating newline) as a freshly-malloc'd block. * Caller is responsible for freeing this block. * Returns 0 on error or EOF. */char *getline(void){char *line;/* line buffer */int n;/* characters read */int size;/* size of line buffer */int c; size =1; line = malloc(size);if(line ==0)return0; n =0;while((c = getchar()) !='\n' && c != EOF) {while(n >= size -1) { size *=2; line = realloc(line, size);if(line ==0)return0; } line[n++] = c; }if(c == EOF && n ==0) {/* got nothing */ free(line);return0; }else { line[n++] = '\0';return line; }}intmain(int argc,char **argv){ Trie t;char *line; t = EMPTY_TRIE;while((line = getline()) !=0) {if(!trie_contains(t, line)) { puts(line); }/* try to insert it either way *//* this tests that insert doesn't blow up on duplicates */ t = trie_insert(t, line); free(line); } puts("==="); trie_print(t); trie_destroy(t);return0;}Tries perform well when all keys are short (or are distinguished by short prefixes), but can grow very large if one inserts two keys that have a long common prefix. The reason is that a trie has to store an internal node for every bit of the common prefix until the two keys become distinguishable, leading to long chains of internal nodes each of which has only one child. An optimization (described inthis paper) known as aPatricia tree eliminates these long chains by having each node store the number of the bit to branch on, like this:
struct patricia_node {char *key;int bit;struct patricia_node *kids[2];};typedefstruct patricia_node *Patricia;Now when searching for a key, instead of using the number of nodes visited so far to figure out which bit to look at, we just read the bit out of the table. This means in particular that we can skip over any bits that we don’t actually branch on. We do however have to be more careful to make sure we don’t run off the end of our target key, since it is possible that when skipping over intermediate bits we might skip over some that distinguish our target from all keys in the table, including longer keys. For example, a Patricia tree storing the stringsabc andabd will first test bit position 22, since that’s whereabc andabd differ. This can be trouble if we are looking forx.
Here’s the search code:
intpatricia_contains(Patricia t,constchar *key){int key_bits; key_bits = BITS_PER_BYTE * (strlen(key)+1);/* +1 for the NUL */while(t && !IsLeaf(t)) {if(t->bit >= key_bits) {/* can't be there */return0; }else { t = t->kids[GET_BIT(key, t->bit)]; } }return t && !strcmp(t->key, key);}The insertion code is similar in many respects to the insertion code for a trie. The differences are that we never construct a long chain of internal nodes when splitting a leaf (although we do have to scan through both the old and new keys to find the first bit position where they differ), but we may sometimes have to add a new internal node between two previously existing nodes if a new key branches off at a bit position that was previously skipped over.
In the worst case Patricia trees are much more efficient than tries, in both space (linear in the number of keys instead of linear in the total size of the keys) and time complexity, often needing to examine only a very small number of bits for misses (hits still require a full scan instrcmp to verify the correct key). The only downside of Patricia trees is that since they work on bits, they are not quite perfectly tuned to the byte or word-oriented structure of modern CPUs.
Ternary search trees were described by Jon Bentley and Bob Sedgewick in an article in the April 1988 issue ofDr. Dobb’s Journal, availablehere.
The basic idea is that each node in the tree stores one character from the key and three child pointerslt,eq, andgt. If the corresponding character in the target is equal to the character in the node, we move to thenext character in the target and follow theeq pointer out of the node. If the target is less, follow thelt pointer but stay at thesame character. If the target is greater, follow thegt pointer and again stay at the same character. When searching for a string, we walk down the tree until we either reach a node that matches the terminating NUL (a hit), or follow a null pointer (a miss).
A TST acts a bit like a 256-way trie, except that instead of storing an array of 256 outgoing pointers, we build something similar to a small binary search tree for the next character. Note that no explicit balancing is done within these binary search trees. From a theoretical perspective, the worst case is that we get a 256-node deep linked-list equivalent at each step, multiplying our search time by256 = O(1). In practice, only those characters that actual appear in some key at this stage will show up, so theO(1) is likely to be a smallO(1), especially if keys are presented in random order.
TSTs are one of the fastest known data structures for implementing dictionaries using strings as keys, beating both hash tables and tries in most cases. They can be slower than Patricia trees if the keys have many keys with long matching prefixes; however, a Patricia-like optimization can be applied to give acompressed ternary search tree that works well even in this case. In practice, regular TSTs are usually good enough.
Here is a simple implementation of an insert-only TST. The C code includes two versions of the insert helper routine; the first is the original recursive version and the second is an iterative version generated by eliminating the tail recursion from the first.
typedefstruct tst_node *TST;#define EMPTY_TST (0)/* returns 1 if t contains target */int tst_contains(TST t,constchar *target);/* add a new key to a TST *//* and return the new TST */TST tst_insert(TST t,constchar *key);/* free a TST */void tst_destroy(TST);#include<stdio.h>#include<stdlib.h>#include<assert.h>#include"tst.h"struct tst_node {char key;/* value to split on */struct tst_node *lt;/* go here if target[index] < value */struct tst_node *eq;/* go here if target[index] == value */struct tst_node *gt;/* go here if target[index] > value */};/* returns 1 if t contains key */inttst_contains(TST t,constchar *key){ assert(key);while(t) {if(*key < t->key) { t = t->lt; }elseif(*key > t->key) { t = t->gt; }elseif(*key == '\0') {return1; }else { t = t->eq; key++; } }return0;}/* original recursive insert */staticvoidtst_insert_recursive(TST *t,constchar *key){if(*t ==0) { *t = malloc(sizeof(**t)); assert(*t); (*t)->key = *key; (*t)->lt = (*t)->eq = (*t)->gt =0; }/* now follow search */if(*key < (*t)->key) { tst_insert_recursive(&(*t)->lt, key); }elseif(*key > (*t)->key) { tst_insert_recursive(&(*t)->gt, key); }elseif(*key == '\0') {/* do nothing, we are done */ ; }else { tst_insert_recursive(&(*t)->eq, key+1); }}/* iterative version of above, since somebody asked *//* This is pretty much standard tail-recursion elimination: *//* The whole function gets wrapped in a loop, and recursive * calls get replaced by assignment */staticvoidtst_insert_iterative(TST *t,constchar *key){for(;;) {if(*t ==0) { *t = malloc(sizeof(**t)); assert(*t); (*t)->key = *key; (*t)->lt = (*t)->eq = (*t)->gt =0; }/* now follow search */if(*key < (*t)->key) { t = &(*t)->lt; }elseif(*key > (*t)->key) { t = &(*t)->gt; }elseif(*key == '\0') {/* do nothing, we are done */return; }else { t = &(*t)->eq; key++; } }}/* add a new key to a TST *//* and return the new TST */TSTtst_insert(TST t,constchar *key){ assert(key);#ifdef USE_RECURSIVE_INSERT tst_insert_recursive(&t, key);#else tst_insert_iterative(&t, key);#endifreturn t;}/* free a TST */voidtst_destroy(TST t){if(t) { tst_destroy(t->lt); tst_destroy(t->eq); tst_destroy(t->gt); free(t); }}And here is some test code, almost identical to the test code for tries:test_tst.c.
TheDr. Dobb’s article contains additional code for doing deletions and partial matches, plus some optimizations for inserts.
The standardquicksort routine is an example of acomparison-based sorting algorithm. This means that the only information the algorithm uses about the items it is sorting is the return value of thecompare routine. This has a rather nice advantage of making the algorithm very general, but has the disadvantage that the algorithm can extract only one bit of information from every call tocompare. Since there aren! possible ways to reorder the input sequence, this means we need at leastlog (n!) = O(nlog n) calls tocompare to finish the sort. If we are sorting something like strings, this can get particularly expensive, because calls tostrcmp may take time linear in the length of the strings being compared. In the worst case, sortingn strings of lengthm each could takeO(nmlog n) time.
The core idea of radix sort is that if we want to sort values from a small range, we can do it by making one bucket for each possible value and throw any object with that value into the corresponding bucket. In the old days, whenSolitaire was a game played with physical pieces of cardboard, a player who suspected that that one of these “cards” had fallen under the couch might sort the deck by dividing it up into Spades, Hearts, Diamonds, and Club piles and then sorting each pile recursively. The same trick works in a computer, but there the buckets are typically implemented as an array of some convenient data structure.
If the number of possible values is too big, we may still be able to use bucket sort digit-by-digit. The resulting algorithms are known generally asradix sort. These are a class of algorithms designed for sorting strings in lexicographic order—the order used by dictionaries where one string is greater than another if the first character on which they differ is greater. One particular variant,most-significant-byte radix sort or MSB radix sort, has the beautiful property that its running time is not only linear in the size of the input in bytes, but is also linear in the smallest number of characters in the input that need to be examined to determine the correct order. This algorithm is so fast that it takes not much more time to sort data than it does to read the data from memory and write it back. But it’s a little trickier to explain that the originalleast-significant-byte radix sort or LSB radix sort.
This is the variant used for punch cards, and works well for fixed-length strings. The idea is to sort on the least significant position first, then work backwards to the most significant position. This works as long as each sort isstable, meaning that it doesn’t reorder values with equal keys. For example, suppose we are sorting the strings:
satbatbadThe first pass sorts on the third column, giving:
badsatbatThe second pass sorts on the second column, producing no change in the order (all the characters are the same). The last pass sorts on the first column. This moves thes after the twobs, but preserves the order of the two words starting withb. The result is:
badbatsatThere are three downsides to LSB radix sort:
ints).aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa andbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, we spend a lot of time matching upas against each other.For these reasons, MSB radix sort is used more often. This is basically the radix sort version ofquicksort, where instead of partitioning our input data into two piles based on whether each element is less than or greater than a random pivot, we partition the input into 256 piles, one for each initial byte. We can then sort each pile recursively using the same algorithm, taking advantage of the fact that we know that the first byte (or later, the first k bytes) are equal and so we only need to look at the next one. The recursion stops when we get down to 1 value, or in practice where we get down to a small enough number of values that the cost of doing a different sorting algorithm becomes lower than the cost of creating and tearing down the data structures for managing the piles.
The depth of recursion for MSB radix sort is equal to the length of the second-longest string in the worst case. Since strings can be pretty long, this creates a danger of blowing out the stack. The solution (as inquicksort) is to use tail recursion for the largest pile. Now any pile we recurse into with an actual procedure call is at most half the size of the original pile, so we get stack depth at mostO(log n).
There is a trick we can do analagous to the Dutch flag algorithm where we sort the array in place. The idea is that we first count the number of elements that land in each bucket and assign a block of the array for each bucket, keeping track in each block of an initial prefix of values that belong in the bucket with the rest not yet processed. We then walk through the buckets swapping out any elements at the top of the good prefix to the bucket they are supposed to be in. This procedure puts at least one element in the right bucket for each swap, so we reorder everything correctly in at mostn swaps andO(n) additional work.
To keep track of each bucket, we use two pointersbucket[i] for the first element of the bucket andtop[i] for the first unused element. We could make these be integer array indices, but this slows the code down by about 10%. This seems to be a situation where our use of pointers is complicated enough that the compiler can’t optimize out the array lookups.
Since we are detecting the heaviest bucket anyway, there is a straightforward optimization that speeds the sort up noticeably on inputs with a lot of duplicates: if everything would land in the same bucket, we can skip the bucket-sort and just go directly to the next character.
Here is an implementation of MSB radix sort using the ideas above:
#include<assert.h>#include<limits.h>#include<string.h>#include"radixSort.h"/* in-place MSB radix sort for null-terminated strings *//* helper routine for swapping */staticvoidswapStrings(constchar **a,constchar **b){constchar *temp; temp = *a; *a = *b; *b = temp;}/* this is the internal routine that assumes all strings are equal for the * first k characters */staticvoidradixSortInternal(int n,constchar **a,int k){int i;int count[UCHAR_MAX+1];/* number of strings with given character in position k */int mode;/* most common position-k character */constchar **bucket[UCHAR_MAX+1];/* position of character block in output */constchar **top[UCHAR_MAX+1];/* first unused index in this character block *//* loop implements tail recursion on most common character */while(n >1) {/* count occurrences of each character */ memset(count,0,sizeof(int)*(UCHAR_MAX+1));for(i =0; i < n; i++) { count[(unsignedchar) a[i][k]]++; }/* find the most common nonzero character *//* we will handle this specially */ mode =1;for(i =2; i < UCHAR_MAX+1; i++) {if(count[i] > count[mode]) { mode = i; } }if(count[mode] < n) {/* generate bucket and top fields */ bucket[0] = top[0] = a;for(i =1; i < UCHAR_MAX+1; i++) { top[i] = bucket[i] = bucket[i-1] + count[i-1]; }/* reorder elements by k-th character *//* this is similar to dutch flag algorithm *//* we start at bottom character and swap values out until everything is in place *//* invariant is that for all i, bucket[i] <= j < top[i] implies a[j][k] == i */for(i =0; i < UCHAR_MAX+1; i++) {while(top[i] < bucket[i] + count[i]) {if((unsignedchar) top[i][0][k] == i) {/* leave it in place, advance bucket */ top[i]++; }else {/* swap with top of appropriate block */ swapStrings(top[i], top[(unsignedchar) top[i][0][k]]++); } } }/* we have now reordered everything *//* recurse on all but 0 and mode */for(i =1; i < UCHAR_MAX+1; i++) {if(i != mode) { radixSortInternal(count[i], bucket[i], k+1); } }/* tail recurse on mode */ n = count[mode]; a = bucket[mode]; k = k+1; }else {/* tail recurse on whole pile */ k = k+1; } }}voidradixSort(int n,constchar **a){ radixSortInternal(n, a,0);}Some additional files:radixSort.h,test_radixSort.c,Makefile,sortInput.c. The last is a program that sorts lines onstdin and writes the result tostdout, similar to the GNUsort utility. When compiled with-O3 and run on my machine, this runs in about the same time as the standardsort program when run on a 4.7 million line input file consisting of a random shuffle of 20 copies of/usr/share/dict/words, providedsort is run withLANG=C sort < /usr/share/dict/words to keep it from having to deal with locale-specific collating issues. On other inputs,sort is faster. This is not bad given how thoroughlysort has been optimized, but is a sign that further optimization is possible.
These are mostly leftovers from previous versions of the class where different topics were emphasized.
Here we show how to implement various mechanisms often found in more sophisticated programming languages in C using function pointers.
Suppose we have an abstract data type that represents some sort of container, such as a list or dictionary. We’d like to be able to do something to every element of the container; say, count them up. How can we write operations on the abstract data type to allow this, without exposing the implementation?
To make the problem more concrete, let’s suppose we have an abstract data type that represents the set of all non-negative numbers less than some fixed bound. The core of its interface might look like this:
/* * Abstract data type representing the set of numbers from 0 to * bound-1 inclusive, where bound is passed in as an argument at creation. */typedefstruct nums *Nums;/* Create a Nums object with given bound. */Nums nums_create(int bound);/* Destructor */void nums_destroy(Nums);/* Returns 1 if nums contains element, 0 otherwise */int nums_contains(Nums nums,int element);#include<stdlib.h>#include"nums.h"struct nums {int bound;};Nums nums_create(int bound){struct nums *n; n = malloc(sizeof(*n)); n->bound = bound;return n;}void nums_destroy(Nums n) { free(n); }int nums_contains(Nums n,int element){return element >=0 && element < n->bound;}From the outside, aNums acts like the set of numbers from0 tobound - 1;nums_contains will insist that it contains anyint that is in this set and contains noint that is not in this set.
Let’s suppose now that we want to loop over all elements of someNums, say to add them together. In particular, we’d like to implement the following pseudocode, wherenums is someNums instance:
One way to do this would be to build the loop into some operation innums.c, including its body. But we’d like to be able to substitute any body for thesum += i line. Since we can’t see the inside of aNums, we need to have some additional operation or operations on aNums that lets us write the loop. How can we do this?
A data-driven approach might be to add anums_contents function that returns a sequence of all elements of some instance, perhaps in the form of an array or linked list. The advantage of this approach is that once you have the sequence, you don’t need to worry about changes to (or destruction of) the original object. The disadvantage is that you have to deal with storage management issues, and have to pay the costs in time and space of allocating and filling in the sequence. This can be particularly onerous for a “virtual” container likeNums, since we could conceivably have aNums instance with billions of elements.
Bearing these facts in mind, let’s see what this approach might look like. We’ll define a new functionnums_contents that returns an array ofints, terminated by a-1 sentinel:
int *nums_contents(Nums n){int *a;int i; a = malloc(sizeof(*a) * (n->bound +1));for(i =0; i < n->bound; i++) a[i] = i; a[n->bound] = -1;return a;}We might use it like this:
sum =0; contents = nums_contents(nums);for(p = contents; *p != -1; p++) { sum += *p; } free(contents);Despite the naturalness of the approach, returning a sequence in this case leads to themost code complexity of the options we will examine.
If we don’t want to look at all the elements at once, but just want to process them one at a time, we can build aniterator. An iterator is an object that allows you to step through the contents of another object, by providing convenient operations for getting the first element, testing when you are done, and getting the next element if you are not. In C, we try to design iterators to have operations that fit well in the top of afor loop.
For theNums type, we’ll make eachNums its own iterator. The new operations are given here:
int nums_first(Nums n) {return0; }int nums_done(Nums n,int val) {return val >= n->bound; }int nums_next(Nums n,int val) {return val+1; }And we use them like this:
Not only do we completely avoid the overhead of building a sequence, we also get much cleaner code. It helps in this case that all we need to find the next value is the previous one; for a more complicated problem we might have to create and destroy a separate iterator object that holds the state of the loop. But for many tasks in C, the first/done/next idiom is a pretty good one.
Suppose we have a very complicated iteration, say one that might require several nested loops or even a recursion to span all the elements. In this case it might be very difficult to provide first/done/next operations, because it would be hard to encode the state of the iteration so that we could easily pick up in the next operation where we previously left off. What we’d really like to do is to be able to plug arbitrary code into the innermost loop of our horrible iteration procedure, and do it in a way that is reasonably typesafe and doesn’t violate our abstraction barrier. This is a job for function pointers, and an example of thefunctional programming style in action.
We’ll define anums_foreach function that takes a function as an argument:
void nums_foreach(Nums n,void (*f)(int,void *),void *f_data){int i;for(i =0; i < n->bound; i++) f(i, f_data);}Thef_data argument is used to pass extra state into the passed-in functionf; it’s avoid * because we want to letf work on any sort of extra state it likes.
Now to do our summation, we first define an extra functionsum_helper, which adds each element to an accumulator pointed to byf_data:
We then feedsum_helper to thenums_foreach function:
There is a bit of a nuisance in having to define the auxiliarysum_helper function and in all the casts to and fromvoid, but on the whole the complexity of this solution is not substantially greater than the first/done/next approach. Which you should do depends on whether it’s harder to encapsulate the state of the iterator (in which case the functional approach is preferable) or of the loop body (in which case the first/done/next approach is preferable), and whether you need to bail out of the loop early (which would require special support from theforeach procedure, perhaps checking a return value from the function). However, it’s almost always straightforward to encapsulate the state of a loop body; just build astruct containing all the variables that it uses, and pass a pointer to this struct asf_data.
Aclosure is a function plus some associated state. A simple way to implement closures in C is to use astatic local variable, but then you only get one. Better is to allocate the state somewhere and pass it around with the function. For example, here’s a simple functional implementation of infinite sequences:
/* a sequence is an object that returns a new value each time it is called */struct sequence {int (*next)(void *data);void *data;};typedefstruct sequence *Sequence;Sequencecreate_sequence(int (*next)(void *data),void *data){ Sequence s; s = malloc(sizeof(*s)); assert(s); s->next = next; s->data = data;return s;}intsequence_next(Sequence s){return s->next(s->data);}And here are some examples of sequences:
/* make a constant sequence that always returns x */staticintconstant_sequence_next(void *data){return *((int *) data);}Sequenceconstant_sequence(int x){int *data; data = malloc(sizeof(*data));if(data ==0)return0; *data = x;return create_sequence(constant_sequence_next, data);}/* make a sequence x, x+a, x+2*a, x+3*a, ... */struct arithmetic_sequence_data {int cur;int step;};staticintarithmetic_sequence_next(void *data){struct arithmetic_sequence_data *d; d = data; d->cur += d->step;return d->cur;}Sequencearithmetic_sequence(int x,int a){struct arithmetic_sequence_data *d; d = malloc(sizeof(*d));if(d ==0)return0; d->cur = x - a;/* back up so first value returned is x */ d->step = a;return create_sequence(arithmetic_sequence_next, d);}/* Return the sum of two sequences */staticintadd_sequences_next(void *data){ Sequence *s; s = data;return sequence_next(s[0]) + sequence_next(s[1]);}Sequenceadd_sequences(Sequence s0, Sequence s1){ Sequence *s; s = malloc(2*sizeof(*s));if(s ==0)return0; s[0] = s0; s[1] = s1;return create_sequence(add_sequences_next, s);}/* Return the sequence x, f(x), f(f(x)), ... */struct iterated_function_sequence_data {int x;int (*f)(int);}staticintiterated_function_sequence_next(void *data){struct iterated_function_sequence_data *d;int retval; d = data; retval = d->x; d->x = d->f(d->x);return retval;}Sequenceiterated_function_sequence(int (*f)(int),int x0){struct iterated_function_sequence_data *d; d = malloc(sizeof(*d));if(d ==0)return0; d->x = x0; d->f = f;return create_sequence(iterated_function_sequence_next, d);}Note that we haven’t worried about how to free thedata field inside aSequence, and indeed it’s not obvious that we can write a generic data-freeing routine since we don’t know what structure it has. The solution is to add more function pointers to aSequence, so that we can get the next value, get the sequence to destroy itself, etc. When we do so, we have gone beyond building a closure to building anobject.
Here’s an example of a hierarchy of counter objects. Each counter object has (at least) three operations:reset,next, anddestroy. To call thenext operation on counterc we includec and the first argument, e.g. c->next(c) (one could write a wrapper to enforce this).
The main trick is that we define a basic counter structure and then extend it to include additional data, using lots of pointer conversions to make everything work.
/* use preprocessor to avoid rewriting these */#define COUNTER_FIELDS \ void (*reset)(struct counter *); \ int (*next)(struct counter *); \ void (*destroy)(struct counter *);struct counter { COUNTER_FIELDS};typedefstruct counter *Counter;/* minimal counter--always returns zero *//* we don't even allocate this, just have one global one */staticvoid noop(Counter c) { ; }staticint return_zero(Counter c) {return0; }staticstruct counter Zero_counter = { noop, return_zero, noop };Countermake_zero_counter(void){return &Zero_counter;}/* a fancier counter that iterates a function sequence *//* this struct is not exported anywhere */struct ifs_counter {/* copied from struct counter declaration */ COUNTER_FIELDS/* new fields */int init;int cur;int (*f)(int);/* update rule */};staticvoidifs_reset(Counter c){struct ifs_counter *ic; ic = (struct ifs_counter *) c; ic->cur = ic->init;}staticvoidifs_next(Counter c){struct ifs_counter *ic;int ret; ic = (struct ifs_counter *) c; ret = ic->cur; ic->cur = ic->f(ic->cur);return ret;}Countermake_ifs_counter(int init,int (*f)(int)){struct ifs_counter *ic; ic = malloc(sizeof(*ic)); ic->reset = ifs_reset; ic->next = ifs_next; ic->destroy = (void (*)(struct counter *)) free; ic->init = init; ic->cur = init; ic->f = f;/* it's always a Counter on the outside */return (Counter) ic;}A typical use might be
staticinttimes2(int x){return x*2;}voidprint_powers_of_2(void){int i; Counter c; c = make_ifs_counter(1, times2);for(i =0; i <10; i++) { printf("%d\n", c->next(c)); } c->reset(c);for(i =0; i <20; i++) { printf("%d\n", c->next(c)); } c->destroy(c);}These are notes on practical implementations of suffix arrays, which are a data structure for searching quickly for substrings of a given large string.
Without preprocessing, searching ann-character string for anm-character substring can be done using algorithms of varying degrees of sophistication, the worst of which run in timeO(nm) (runstrncmp on each position in the big string), and best of which run in timeO(n + m) (run theBoyer-Moore string search algorithm). But we are interested in the case where we can preprocess our big string into a data structure that will let us do lots of searches for cheap.
Suffix trees andsuffix arrays are data structures for representing texts that allow substring queries like “where does this pattern appear in the text” or “how many times does this pattern occur in the text” to be answered quickly. Both work by storing all suffixes of a text, where asuffix is a substring that runs to the end of the text. Of course, storing actual copies of all suffixes of ann-character text would takeO(n2) space, so instead each suffix is represented by a pointer to its first character.
A suffix array stores all the suffixes sorted in dictionary order. For example, the suffix array of the stringabracadabra is shown below. The actual contents of the array are the indices in the left-hand column; the right-hand shows the corresponding suffixes.
11 \010 a\0 7 abra\0 0 abracadabra\0 3 acadabra\0 5 adabra\0 8 bra\0 1 bracadabra\0 4 cadabra\0 6 dabra\0 9 ra\0 2 racadabra\0A suffix tree is similar, but instead using an array, we use some sort of tree data structure to hold the sorted list. A common choice given an alphabet of some fixed sizek is atrie, in which each node at depthd represents a string of lengthd, and its up tok children represent all(d + 1)-character extensions of the string. The advantage of using a suffix trie is that searching for a string of lengthm takesO(m) time, since we can just walk down the trie at the rate of one node per character inm. A further optimization is to replace any long chain of single-child nodes with acompressed edge labeled with the concatenation all the characters in the chain. Such compressed suffix tries can not only be searched in linear time but can also be constructed in linear time with a sufficiently clever algorithm (we won’t actually do this here). Of course, we could also use a simple balanced binary tree, which might be preferable if the alphabet is large.
The disadvantage of suffix trees over suffix arrays is that they generally require more space to store all the internal pointers in the tree. If we are indexing a huge text (or collection of texts), this extra space may be too expensive.
A straightforward approach to building a suffix array is to run any decent comparison-based sorting algorithm on the set of suffixes (represented by pointers into the text). This will takeO(nlog n) comparisons, but in the worst case each comparison will takeO(n) time, for a total ofO(n2log n) time. This is the approach used in the sample code below.
The original suffix array paper by Manber and Myers (“Suffix arrays: a new method for on-line string searches,” SIAM Journal on Computing 22(5):935-948, 1993) gives anO(nlog n) algorithm, somewhat resembling radix sort, for building suffix arrays in place with only a small amount of additional space. They also note that for random text, simple radix sorting works well, since most suffixes become distinguishable after aboutlogkn characters (wherek is the size of the alphabet); this gives a cost ofO(nlog n) to do the sort, since radix sort only looks at the bytes it needs to once. For a comparison-based sort, the same assumption gives anO(nlog2n) running time; this is a factor oflog n slower, but this may be acceptable if programmer time is more important.
The fastest approach is to build a suffix tree inO(n) time and extract the suffix array by traversing the tree. The only complication is that we need the extra space to build the tree, although we get it back when we throw the tree away.
Suppose we have a suffix array corresponding to ann-character text and we want to find all occurrences in the text of anm-character pattern. Since the suffixes are ordered, the easiest solution is to do binary search for the first and last occurrences of the pattern (if any) usingO(log n) comparisons. (The code below does something even lazier than this, searching for some match and then scanning linearly for the first and last matches.) Unfortunately, each comparison may take as much asO(m) time, since we may have to check allm characters of the pattern. So the total cost will beO(mlog n) in the worst case.
By storing additional information about the longest common prefix of consecutive suffixes, it is possible to avoid having to re-examine every character in the pattern for every comparison, reducing the search cost toO(m + log n). With a sufficiently clever algorithm, this information can be computed in linear time, and can also be used to solve quickly such problems as finding the longest duplicate substrings, or most frequently occurring strings. For more details, see (Gusfield, Dan.Algorithms on Strings, Trees, and Sequences: Computer Science and Computational Biology. Cambridge University Press, 1997, §7.14.4]).
Using binary search on the suffix array, most searching tasks are now easy:
Closely related to suffix arrays is theBurrows-Wheeler transform (Burrows and Wheeler,A Block-Sorting Lossless Data Compression Algorithm, DEC Systems Research Center Technical Report number 124, 1994), which is the basis for some of the best currently known algorithms for text compression (it’s the technique that is used, for example, inbzip2).
The idea of the Burrows-Wheeler Transform is to construct an array whose rows are all cyclic shifts of the input string in dictionary order, and return the last column of the array. The last column will tend to have long runs of identical characters, since whenever some substring (likethe appears repeatedly in the input, shifts that put the first charactert in the last column will put the rest of the substringhe in the first columns, and the resulting rows will tend to be sorted together. The relative regularity of the last column means that it will compress well with even very simple compression algorithms like run-length encoding.
Below is an example of the Burrows-Wheeler transform in action, with$ marking end-of-text. The transformed value ofabracadabra$ is$drcraaaabba, the last column of the sorted array; note the long run of a’s (and the shorter run of b’s).
abracadabra$ abracadabra$bracadabra$a abra$abracadracadabra$ab acadabra$abracadabra$abr adabra$abraccadabra$abra a$abracadabradabra$abrac bracadabra$adabra$abraca --> bra$abracadaabra$abracad cadabra$abrabra$abracada dabra$abracara$abracadab racadabra$aba$abracadabr ra$abracadab$abracadabra $abracadabraThe most useful property of the Burrows-Wheeler transform is that it can be inverted; this distinguishes it from other transforms that produce long runs like simply sorting the characters. We’ll describe two ways to do this; the first is less efficient, but more easily grasped, and involves rebuilding the array one column at a time, starting at the left. Observe that the leftmost column is just all the characters in the string in sorted order; we can recover it by sorting the rightmost column, which we have to start off with. If we paste the rightmost and leftmost columns together, we have the list of all 2-character substrings of the original text; sorting this list gives the firsttwo columns of the array. (Remember that each copy of the string wraps around from the right to the left.) We can then paste the rightmost column at the beginning of these two columns, sort the result, and get the first three columns. Repeating this process eventually reconstructs the entire array, from which we can read off the original string from any row. The initial stages of this process forabracadabra$ are shown below:
$ a $a ab $ab abrd a da ab dab abrr a ra ac rac acac a ca ad cad adar a ra a$ ra$ a$aa b ab br abr braa -> b ab -> br abr -> braa c ac ca aca cada d ad da ada dabb r br ra bra racb r br ra bra ra$a $ a$ $a a$a $abRebuilding the entire array in this fashion takesO(n2) time andO(n2) space. In their paper, Burrows and Wheeler showed that one can in fact reconstruct the original string from just the first and last columns in the array inO(n) time.
Here’s the idea: Suppose that all the characters were distinct. Then after reconstructing the first column we would know all pairs of adjacent characters. So we could just start with the last character$ and regenerate the string by appending at each step the unique successor to the last character so far. If all characters were distinct, we would never get confused about which character comes next.
The problem is what to do with pairs with duplicate first characters, likeab andac in the example above. We can imagine that eacha in the last column is labeled in some unique way, so that we can talk about the firsta or the thirda, but how do we know whicha is the one that comes beforeb ord?
The trick is to look closely at how the original sort works. Look at the rows in the original transformation. If we look at all rows that start witha, the order they are sorted in is determined by the suffix aftera. These suffixes also appear as the prefixes of the rows thatend witha, since the rows that end witha are just the rows that start witha rotated one position. It follows thatall instances of the same letter occur in the same order in the first and last columns. So if we use a stable sort to construct the first column, we will correctly match up instances of letters.
This method is shown in action below. Each letter is annotated uniquely with a count of how many identical letters equal or precede it. Sorting recovers the first column, and combining the last and first columns gives a list of unique pairs of adjacent annotated characters. Now start with$1 and construct the full sequence$1 a1 b1 r1 a3 c1 a4 d1 a2 b2 r2 a5 $1. The original string is obtained by removing the end-of-string markers and annotations:abracadabra.
$1 a1d1 a2r1 a3c1 a4r2 a5a1 b1a2 --> b2a3 c1a4 d1b1 r1b2 r2a5 $1Because we are only sorting single characters, we can perform the sort in linear time using counting sort. Extracting the original string also takes linear time if implemented reasonably.
A useful property of the Burrows-Wheeler transform is that each row of the sorted array is essentially the same as the corresponding row in the suffix array, except for the rotated string prefix after the$ marker. This means, among other things, that we can compute the Burrows-Wheeler transform in linear time using suffix trees. Ferragina and Manzini (http://people.unipmn.it/~manzini/papers/focs00draft.pdf) have further exploited this correspondence (and some very clever additional ideas) to design compressed suffix arrays that compress and index a text at the same time, so that pattern searches can be done directly on the compressed text in time close to that needed for suffix array searches.
As mentioned above, this is a pretty lazy implementation of suffix arrays, that doesn’t include many of the optimizations that would be necessary to deal with huge source texts.
/* we expose this so user can iterate through it */struct suffixArray {size_t n;/* length of string INCLUDING final null */constchar *string;/* original string */constchar **suffix;/* suffix array of length n */};typedefstruct suffixArray *SuffixArray;/* construct a suffix array *//* it is a bad idea to modify string before destroying this */SuffixArray suffixArrayCreate(constchar *string);/* destructor */void suffixArrayDestroy(SuffixArray);/* return number of occurrences of substring *//* if non-null, index of first occurrence is place in first */size_tsuffixArraySearch(SuffixArray,constchar *substring,size_t *first);/* return the Burrows-Wheeler transform of the underlying string * as malloc'd data of length sa->n *//* note that this may have a null in the middle somewhere */char *suffixArrayBWT(SuffixArray sa);/* invert BWT of null-terminated string, returning a malloc'd copy of original */char *inverseBWT(size_t len,constchar *s);#include<stdlib.h>#include<assert.h>#include<string.h>#include<limits.h>#include"suffixArray.h"staticintsaCompare(constvoid *s1,constvoid *s2){return strcmp(*((constchar **) s1), *((constchar **) s2));}SuffixArraysuffixArrayCreate(constchar *s){size_t i; SuffixArray sa; sa = malloc(sizeof(*sa)); assert(sa); sa->n = strlen(s) +1; sa->string = s; sa->suffix = malloc(sizeof(*sa->suffix) * sa->n); assert(sa->suffix);/* construct array of pointers to suffixes */for(i =0; i < sa->n; i++) { sa->suffix[i] = s+i; }/* this could be a lot more efficient */ qsort(sa->suffix, sa->n,sizeof(*sa->suffix), saCompare);return sa;}voidsuffixArrayDestroy(SuffixArray sa){ free(sa->suffix); free(sa);}size_tsuffixArraySearch(SuffixArray sa,constchar *substring,size_t *first){size_t lo;size_t hi;size_t mid;size_t len;int cmp; len = strlen(substring);/* invariant: suffix[lo] <= substring < suffix[hi] */ lo =0; hi = sa->n;while(lo +1 < hi) { mid = (lo+hi)/2; cmp = strncmp(sa->suffix[mid], substring, len);if(cmp ==0) {/* we have a winner *//* search backwards and forwards for first and last */for(lo = mid; lo >0 && strncmp(sa->suffix[lo-1], substring, len) ==0; lo--);for(hi = mid; hi < sa->n && strncmp(sa->suffix[hi+1], substring, len) ==0; hi++);if(first) { *first = lo; }return hi - lo +1; }elseif(cmp <0) { lo = mid; }else { hi = mid; } }return0;}char *suffixArrayBWT(SuffixArray sa){char *bwt;size_t i; bwt = malloc(sa->n); assert(bwt);for(i =0; i < sa->n; i++) {if(sa->suffix[i] == sa->string) {/* wraps around to nul */ bwt[i] = '\0'; }else { bwt[i] = sa->suffix[i][-1]; } }return bwt;}char *inverseBWT(size_t len,constchar *s){/* basic trick: stable sort of s gives successor indices *//* then we just thread through starting from the nul */size_t *successor;int c;size_t count[UCHAR_MAX+1];size_t offset[UCHAR_MAX+1];size_t i;char *ret;size_t thread; successor = malloc(sizeof(*successor) * len); assert(successor);/* counting sort */for(c =0; c <= UCHAR_MAX; c++) { count[c] =0; }for(i =0; i < len; i++) { count[(unsignedchar) s[i]]++; } offset[0] =0;for(c =1; c <= UCHAR_MAX; c++) { offset[c] = offset[c-1] + count[c-1]; }for(i =0; i < len; i++) { successor[offset[(unsignedchar) s[i]]++] = i; }/* find the nul */for(thread =0; s[thread]; thread++);/* thread the result */ ret = malloc(len); assert(ret);for(i =0, thread = successor[thread]; i < len; i++, thread = successor[thread]) { ret[i] = s[thread]; }return ret;}Here is a Makefile and test code:Makefile,testSuffixArray.c.
The output ofmake test shows all occurrences of a target string, the Burrows-Wheeler transform of the source string (second-to-last line), and its inversion (last line, which is just the original string):
$ make test/bin/echo -n abracadabra-abracadabra-shmabracadabra | ./testSuffixArray abraCount: 6abraabra-abrabra-shmabracadaabracadaabracadaaaarrrdddm\x00-rrrcccaaaaaaaaaaaashbbbbbb-abracadabra-abracadabra-shmabracadabraHere we will describe some basic features of C++ that are useful for implementing abstract data types. Like all programming languages, C++ comes with an ideology, which in this case emphasizes object-oriented features like inheritance. We will be ignoring this ideology and treating C++ as an improved version of C.
The goal here is not to teach you all of C++, which would take a while, but instead to give you some hints for why you might want to learn C++ on your own. If you decide to learn C++ for real, Bjarne Stroustrup’sThe C++ Programming Language is the definitive source. A classic tutorialhere aimed at C programmers introduces C++ features one at a time (some of these features have since migrated into C). The web sitehttp://www.cplusplus.com has extensive tutorials and documentation.
The C++ version of “hello world” looks like this:
Compile this usingg++ instead ofgcc. Make shows how it is done:
$ make helloworldg++ helloworld.cpp -o helloworldOr we could use an explicitMakefile:
CPP=g++CPPFLAGS=-g3 -Wallhelloworld: helloworld.o $(CPP) $(CPPFLAGS) -o $@ $^Now the compilation looks like this:
$ make helloworldg++ -g3 -Wall -c -o helloworld.o helloworld.cppg++ -g3 -Wall -o helloworld helloworld.oThe main difference from the C version:
#include <stdio.h> is replaced by#include <iostream>, which gets the C++ version of thestdio library.printf("hi\n") is replaced bystd::cout << "hi\n". The streamstd::cout is the C++ wrapper forstdout; you should read this variable name ascout in thestd namespace. The<< operator is overloaded for streams so that it sends its right argument out on its left argument (see the discussion of operator overloading below). You can also do things likestd::cout << 37,std::cout << 'q',std::cout << 4.7, etc. These all do pretty much what you expect.If you don’t like typingstd:: before all the built-in functions and variables, you can putusing namespace std somewhere early in your program, like this:
Recall that in C we sometime pass objects into function by reference instead of by value, by using a pointer:
This becomes even more useful in C++, since many of the objects we are dealing with are quite large, and can defend themselves against dangerous modifications by restricting access to their components. So C++ provides a special syntax allowing function parameters to be declared as call-by-reference rather than call-by-value. The function above could be rewritten in C++ as
Theint &x declaration says thatx is areference to whatever variable is passed as the argument toincrement. A reference acts exactly like a pointer that has already had* applied to it. You can even write&x to get a pointer to the original variable if you want to for some reason.
As with pointers, it’s polite to mark a reference withconst if you don’t intend to modify the original object:
References are also used as a return type to chain operators together; in the expression
the return type of the first<< operator is anostream & reference (as iscout); this means that the'\n' gets sent to the same object. We could make the return value be just anostream, but thencout would be copied, which could be expensive and would mean that the copy was no longer working on the same internal state as the original. This same trick is used when overloading the assignment operator.
C++ lets you define multiple functions with the same name, where the choice of which function to call depends on the type of its arguments. Here is a program that demonstrates this feature:
#include<iostream>usingnamespace std;constchar *typeName(int x){return"int";}constchar *typeName(double x){return"double";}constchar *typeName(char x){return"char";}intmain(int argc,constchar **argv){ cout <<"The type of " <<3 <<" is " << typeName(3) <<".\n"; cout <<"The type of " <<3.1 <<" is " << typeName(3.1) <<".\n"; cout <<"The type of " <<'c' <<" is " << typeName('c') <<".\n";return0;}And here is what it looks like when we compile and run it:
$ make functionOverloadingg++ functionOverloading.cpp -o functionOverloading$ ./functionOverloading The type of 3 is int.The type of 3.1 is double.The type of c is char.Internally,g++ compiles three separate functions with different (and ugly) names, and when you usetypeName on an object of a particular type,g++ picks the one whose type matches. This is similar to what happens with built-in operators in straight C, where+ means different things depending on whether you apply it to a pair ofints, a pair ofdoubles, or a pointer and anint, but C++ lets you do it with your own functions.
C++ allows you to declareclasses that look suspiciously like structs. The main differences between a class and a C-style struct are that (a) classes providemember functions ormethods that operate on instances of the class and that are called using a struct-like syntax; and (b) classes can distinguish between private members (only accessible to methods of the class) and public members (accessible to everybody).
In C, we organize abstract data types by putting the representation in a struct and putting the operations on the data type in functions that work on this struct, often giving the functions a prefix that hints at the type of its target (mostly to avoid namespace collisions). Classes in C++ make this connection between a data structure and the operations on it much more explicit.
Here is a simple example of a C++ class in action:
#include<iostream>usingnamespace std;/* counters can be incremented or read */class Counter {int value;/* private value */public: Counter();/* constructor with default value */ Counter(int);/* constructor with specified value */ ~Counter();/* useless destructor */int read();/* get the value of the counter */void increment();/* add one to the counter */};Counter::Counter() { value =0; }Counter::Counter(int initialValue) { value = initialValue; }Counter::~Counter() { cerr <<"counter de-allocated with value " << value <<'\n'; }int Counter::read() {return value; }void Counter::increment() { value++; }intmain(int argc,constchar **argv){ Counter c; Counter c10(10); cout <<"c starts at " << c.read() <<'\n'; c.increment(); cout <<"c after one increment is " << c.read() <<'\n'; cout <<"c10 starts at " << c10.read() <<'\n'; c.increment(); c.increment(); cout <<"c10 after two increments is " << c10.read() <<'\n';return0;}Things to notice:
class Counter declaration, thepublic: label introduces the public members of the class. The membervalue is only accessible to member functions ofCounter. This enforces much stronger information hiding than the default in C, although one can still usevoid * trickery to hunt down and extract supposedly private data in C++ objects.:: as inCounter::read.struct access syntax, as inc.read(). Conceptually, each instance of a class has its own member functions, so thatc.read is the function for readingc whilec10.read is the function for readingc10. Inside a member function, names of class members refer to members of the current instance;value insidec.read isc.value (which otherwise is not accessible, sincec.value is not public).Counter::Counter() andCounter::Counter(int). These areconstructors, and are identifiable as such because they are named after the class. A constructor is called whenever a new instance of the class is created. If you create an instance with no arguments (as in the declarationCounter c;), you get the constructor with no arguments. If you create an instance with arguments (as in the declarationCounter c10(10);), you get the version with the appropriate arguments. This is just another example of function overloading. If you don’t define any constructors, C++ supplies a default constructor that takes no arguments and does nothing. Note that constructors don’t have a return type (you don’t need to preface them with void).Counter::~Counter() is adestructor; it is called when an object of typeCounter is de-allocated (say, when returning from a function with a local variable of this type). This particular destructor is not very useful. Destructors are mostly important for objects that allocate their own storage that needs to be de-allocated when the object is; see the section on storage allocation below.Compiling and running this program gives the following output. Note that the last two lines are produced by the destructor.
c starts at 0c after one increment is 1c10 starts at 10c10 after two increments is 10counter de-allocated with value 10counter de-allocated with value 3One subtle difference between C and C++ is that C++ uses empty parentheses() for functions with no arguments, where C would use(void). This is a bit of a historical artifact, having to do with C allowing() for functions whose arguments are not specified in the declaration (which was standard practice before ANSI C).
Curiously, C++ also allows you to declarestructs, with the interpretation that astruct is exactly like aclass except that all members are public by default. So if you changeclass tostruct in the program above, it will do exactly the same thing. In practice, nobody who codes in C++ does this; the feature is mostly useful to allow C code withstructs to mix with C++ code.
Sometimes when you define a new class, you also want to define new interpretations of operators on that class. Here is an example of a class that defines elements of themax-plus algebra overints. This gives us objects that act likeints, except that the+ operator now returns the larger of its arguments and the* operator now returns the sum.25
The mechanism in C++ for doing this is to define member functions with namesoperatorsomething wheresomething is the name of the operator we want to define. These member functions take one less argument that the operator they define; in effect,x + y becomes syntactic sugar forx.operator+(y) (which, amazingly, is actually legal C++). Because these are member functions, they are allowed to access members of other instances of the same class that would normally be hidden.
This same mechanism is also used to define automatic type conversions out of a type: theMaxPlus::operator int() function allows C++ to convert aMaxPlus object to anint whenever it needs to (for example, to feed it tocout). (Automatic type conversionsinto a type happen if you provide an appropriate constructor.)
#include<iostream>#include<algorithm>// for maxusingnamespace std;/* act like ints, except + does max and * does addition */class MaxPlus {int value;public: MaxPlus(int); MaxPlusoperator+(const MaxPlus &); MaxPlusoperator*(const MaxPlus &);operatorint();};MaxPlus::MaxPlus(int x) { value = x; }MaxPlusMaxPlus::operator*(const MaxPlus &other){return MaxPlus(value + other.value);}MaxPlusMaxPlus::operator+(const MaxPlus &other){/* std::max does what you expect */return MaxPlus(max(value, other.value));}MaxPlus::operatorint() {return value; }intmain(int argc,constchar **argv){ cout <<"2+3 == " << (MaxPlus(2) + MaxPlus(3)) <<'\n'; cout <<"2*3 == " << (MaxPlus(2) * MaxPlus(3)) <<'\n';return0;}Avoid the temptation to overuse operator overloading, as it can be dangerous if used to obfuscate what an operator normally does:
The general rule of thumb is that you should probably only do operator overloading if you really are making things that act like numbers (yes,cout << violates this).
Automatic type conversions can be particularly dangerous. The line
is ambiguous: should the compiler convertMaxPlus(2) to anint using theMaxPlus(int) constructor and use ordinary integer addition or convert3 to aMaxPlus usingMaxPlus::operator int() and use funkyMaxPlus addition? Fortunately most C++ compilers will complain about the ambiguity and fail rather than guessing wrong.
One of the things we kept running into in this class was that if we defined a container type like a hash table, binary search tree, or priority queue, we had to either bake in the type of the data it held or do horrible tricks withvoid * pointers to work around the C type system. C++ includes a semi-principled work-around for this problem known astemplates. These are essentially macros that take a type name as an argument, that are expanded as needed to produce functions or classes with specific types (seeMacros for an example of how to do this if you only have C).
Typical use is to prefix a definition withtemplate <class T> and then useT as a type name throughout:
Note the explicit cast toT of1; this avoids ambiguities that might arise with automatic type conversions.
If you put this definition in a program, you can then applyadd1 to any type that has a+ operator and that you can convert1 to. For example, the output of this code fragment:
cout <<"add1(3) == " << add1(3) <<'\n'; cout <<"add1(3.1) == " << add1(3.1) <<'\n'; cout <<"add1('c') == " << add1('c') <<'\n'; cout <<"add1(MaxPlus(0)) == " << add1(MaxPlus(0)) <<'\n'; cout <<"add1(MaxPlus(2)) == " << add1(MaxPlus(2)) <<'\n';is
add1(3) == 4add1(3.1) == 4.1add1('c') == dadd1(MaxPlus(0)) == 1add1(MaxPlus(2)) == 2By default, C++ will instantiate a template to whatever type fits in its argument. If you want to force a particular version, you can put the type in angle brackets after the name of whatever you defined. For example,
produces
add1<int>(3.1) == 4becauseadd1<int> forces its argument to be converted to anint (truncating to3) before adding one to it.
Because templates are really macros that get expanded as needed, it is common to put templates in header (.h) files rather than in.cpp files. See the stack implementation below for an example of this.
C provides no built-in mechanism for signaling that something bad happened. So C programmers are left to come up with ad-hoc mechanisms like:
abort to end the program, either directly or viaassert.exit with a nonzero exit code.-1, etc.)C++ provides a standard mechanism for signaling unusual events known asexceptions. The actual mechanism is similar toreturn: thethrow statement throws an exception that may be caught by atry..catch statement anywhere above it on the execution stack (not necessarily in the same function). Example:
#include<iostream>usingnamespace std;int fail(){throw"you lose";return5;}intmain(int argc,constchar **argv){try { cout << fail() <<'\n'; }catch(constchar *s) { cerr <<"Caught error: " << s <<'\n'; }return0;}In action:
$ make exceptiong++ -g3 -Wall exception.cpp -o exception$ ./exceptionCaught error: you loseNote the use ofcerr instead ofcout. This sends the error message tostderr.
Atry..catch statement will catch an exception only if the type matches the type of the argument to thecatch part of the statement. This can be used to pick and choose which exceptions you want to catch. Seehttp://www.cplusplus.com/doc/tutorial/exceptions/ for some examples and descriptions of some C++ standard library exceptions.
C++ programs generally don’t usemalloc andfree, but instead use the built-in C++ operatorsnew anddelete. The advantage ofnew anddelete is that they know about types: not only does this mean that you don’t have to play games withsizeof to figure out how much space to allocate, but if you allocate a new object from a class with a constructor, the constructor gets called to initialize the object, and if you delete an object, its destructor (if it has one) is called.
There are two versions ofnew anddelete, depending on whether you want to allocate just one object or an array of objects, plus some special syntax for passing constructor arguments:
newtype.newtype[size]. As withmalloc, both operations return a pointer totype.newtype(args). This only works with the single-object version, so you can’t donew SomeClass[12] unlessSomeClass has a constructor that takes no arguments.deletepointer-to-object.delete []pointer-to-base-of-array. Mixingnew withdelete [] or vice versa is an error that may or may not be detected by the compiler. Mixing either withmalloc orfree is a very bad idea.The program below gives examples ofnew anddelete in action:
#include<iostream>#include<cassert>usingnamespace std;class Noisy {int id;public: Noisy(int);// create a noisy object with this id ~Noisy();};Noisy::Noisy(int initId) { id = initId; cout <<"Noisy object created with id " << id <<'\n';}Noisy::~Noisy() { cout <<"Noisy object destroyed with id " << id <<'\n';}intmain(int argc,constchar **argv){int *p;int *a;constint n =100; Noisy n1(1); Noisy *n2; p =newint; a =newint[n]; n2 =new Noisy(2); *p =5;assert(*p ==5);for(int i =0; i < n; i++) { a[i] = i; }for(int i =0; i < n; i++) {assert(a[i] == i); }delete [] a;delete p;delete n2;return0;}Inside objects, storage allocation gets complicated. The reason is that if the object is copied, either by an assignment or by being passed as a call-by-value parameter, the storage pointed to by the object will not be copied. This can lead to two different objects that share the same internal data structures, which is usually not something you want. Furthermore, when the object is deallocated, it’s necessary to also deallocate any space it allocated, which can be done inside the object’s destructor.
To avoid all these problems, any object of typeT that usesnew needs to have all of:
T::~T().T::T(const T &), which is a constructor that takes a reference to another object of the same type as an argument and copies its contents.T::operator=(const T &) that does the same thing, but also deallocates any internal storage of the current object before copying new data in place of it (or possibly just copies the contents of internal storage without doing any allocation and deallocation). The overloaded assignment operator is particularly tricky, because you have to make sure it doesn’t destroy the contents of the object if somebody writes the useless self-assignmenta = a, and you also need to return a reference to*this so that you can chain assignments together as ina = b = c.Here is an example of aStack class that includes all of these members. Note that it is defined using templates so we can make a stack of any type we like.
template <class T>class Stack {staticconstint initialSize =32;/* static means this is shared across entire class */int top;int size; T* contents;public: Stack();/* create a new empty stack *//* the unholy trinity of complex C++ objects */ ~Stack();/* destructor */ Stack(const Stack &);/* copy constructor */ Stack&operator=(const Stack &);/* overloaded assignment */void push(T);/* push an element onto the stack */int isEmpty();/* return 1 if empty */ T pop();/* pop top element from stack */};template <class T>Stack<T>::Stack(){ size = initialSize; top =0; contents =new T[size];}template <class T>Stack<T>::~Stack(){delete [] contents;}template <class T>Stack<T>::Stack(const Stack<T> &other){ size = other.size; top = other.top; contents =new T[size];for(int i =0; i < top; i++) { contents[i] = other.contents[i]; }}template <class T>Stack<T> &Stack<T>::operator=(const Stack<T> &other){if(&other !=this) {/* this is a real assignment */delete [] contents; size = other.size; top = other.top; contents =new T[size];for(int i =0; i < top; i++) { contents[i] = other.contents[i]; } }return *this;}template <class T>voidStack<T>::push(T elt){if(top >= size) {int newSize =2*size; T *newContents =new T[newSize];for(int i =0; i < top; i++) { newContents[i] = contents[i]; }delete [] contents; contents = newContents; size = newSize; } contents[top++] = elt;}template <class T>TStack<T>::pop(){if(top >0) {return contents[--top]; }else {throw"stack empty"; }}Here is some code demonstrating use of the stack:
#include<iostream>#include"stack.h"usingnamespace std;intmain(int argc,constchar **argv){ Stack<int> s; Stack<int> s2;try { s.push(1); s.push(2); s.push(3); s2 = s; cout << s.pop() <<'\n'; cout << s.pop() <<'\n'; cout << s.pop() <<'\n'; cout << s2.pop() <<'\n'; cout << s2.pop() <<'\n'; cout << s2.pop() <<'\n';try { s2.pop(); }catch(constchar *err) { cout <<"Caught expected exception " << err <<'\n'; }for(int i =0; i <1000; i++) { s.push(i); } cout << s.pop() <<'\n'; }catch(constchar *err) { cerr <<"Caught error " << err <<'\n'; }return0;}C++ has a large standard library that includes implementations of many of the data structures we’ve seen in this class. In most situations, it is easier to use the standard library implementations than roll your own, although you have to be careful to make sure you understand just what the standard library implementations do. For example, here is a reimplementation of the main routine fromtestStack.cpp using thestack template from#include <stack>.
#include<iostream>#include<stack>usingnamespace std;intmain(int argc,constchar **argv){ stack<int> s; stack<int> s2; s.push(1); s.push(2); s.push(3); s2 = s; cout << s.top() <<'\n'; s.pop(); cout << s.top() <<'\n'; s.pop(); cout << s.top() <<'\n'; s.pop(); cout << s2.top() <<'\n'; s2.pop(); cout << s2.top() <<'\n'; s2.pop(); cout << s2.top() <<'\n'; s2.pop();for(int i =0; i <1000; i++) { s.push(i); } cout << s.top() <<'\n';return0;}One difference between the standard stack and our stack is thatstd::stack’spop member function doesn’t return anything. So we have to usetop to get the top element before popping it.
There is a chart of all the standard library data structures athttp://www.cplusplus.com/reference/stl/.
The main thing we’ve omitted here is any discussion of object-oriented features of C++, particularly inheritance. These are not immediately useful for the abstract-data-type style of programming we’ve used in CS223, but can be helpful for building more complicated systems, where we might want to have various specialized classes of objects that can all be approached using a common interface represented by a class that they inherit from. If you are interested in exploring these tools further, the CS department occasionally offers a class on object-oriented programming; Mike Fischer’s lecture notes from the last time this course was offered can be found athttp://zoo.cs.yale.edu/classes/cs427/2011a/lectures.html.
It is a truth universally acknowledged that test code should be written early in the development process. Unfortunately, most programmers (including me) tend to assume that a program will work on the first attempt and there’s not much point in testing it anyway, so writing and running test code often gets deferred indefinitely. The solution is to write the test code first, and run it directly from yourMakefile every time you save and compile your program. Not only will this guarantee that your program actually works when you are done (or at least passes the tests you thought of), it allows you to see how the program is improving with each positive change, and prevents you from accidentally making new negative changes that break things that used to work.
Going one step further, we can often write our interface and test code first, build a non-working stub implementation, and then slowly flesh out the missing pieces until the implementation passes all the tests. This way there is always some obvious step to do next, and we don’t find ourselves stuck staring at an empty file.
A straightforward approach to testing is to include test code with everyunit in your program, where a unit is any part of the program that can be sensibly run by itself. Typically, this will be a single function or a group of functions that together implement some data structure.
In C, these will often make up the contents of a single source file. Though this is probably not the best approach if you are building a production-quality testing framework, a simple way to include unit tests in a program is to append to each source file a testmain function that can be enabled by defining a macro (I likeTEST_MAIN). You can then build this file by itself with the macro defined to get a stand-alone test program for just this code.
Ideally, you want to use enough different inputs that every line of code in your program is reached by some test, a goal calledcode coverage. For complex programs, this may be hard to achieve, and there are programs, such as thegcov program that comes withgcc, that will analyze how much code coverage you get out of your tests. For simple programs, we can just try to come up with a set of inputs that covers all our bases.
Testing can be done asblack-box testing, where the test code assumes no knowledge of the implementation, orwhite-box testing, where the test code has direct access to the implementation and can observe the effects of its actions. Black-box testing is handy if your implementation may change, and it is generally a good idea to write black-box tests first. White-box testing can be useful if some states of the data structure are hard to reach otherwise, or if black-box testing is not very informative about why a particular operation is failing. The example given below uses both.
Here is an example of a simple data structure with some built-in test code conditionally compiled by definingTEST_MAIN. The data structure implements a counter with built-in overflow protection. The counter interface does not provide the ability to read the counter value; instead, the user can only tell if it is zero or not.
Because the counter is implemented internally as auint64_t, black-box testing of what happens with too many increments would take centuries. So we include some white-box tests that directly access the counter value to set up this (arguably unnecessary) test case.
The code is given below. We include both the interface file and the implementation, as well as aMakefile showing how to build and run the test program. TheMakefile includes some extra arguments togcc to turn on theTEST_MAIN macro and supply the extra information needed to rungcov. If you typemake test, it will make and runtestCounter, and then rungcov to verify that we did in fact hit all lines of code in the program.
/* * Abstract counter type. * * You can increment it, decrement it, and test for zero. * * Increment and decrement operations return 1 if successful, * 0 if the operation would cause underflow or overflow. */typedefstruct counter Counter;/* make a new counter starting at 0 */Counter *counterCreate(void);/* destroy a counter */void counterDestroy(Counter *);/* return 1 if counter is 0, 0 otherwise */int counterIsZero(const Counter *);/* increment a counter, returns 1 if successful, 0 if increment would cause overflow */int counterIncrement(Counter *);/* decrement a counter, returns 1 if successful, 0 if decrement would cause underflow */int counterDecrement(Counter *);#include<stdio.h>#include<stdlib.h>#include<assert.h>#include"counter.h"#include<stdint.h>#define COUNTER_MAX (UINT64_MAX)struct counter {uint64_t value;};/* make a new counter starting at 0 */Counter *counterCreate(void){ Counter *c; c = malloc(sizeof(Counter)); assert(c); c->value =0;return c;}/* destroy a counter */voidcounterDestroy(Counter *c){ free(c);}/* return 1 if counter is 0, 0 otherwise */intcounterIsZero(const Counter *c){return c->value ==0;}/* increment a counter, returns 1 if successful, 0 if increment would cause overflow */intcounterIncrement(Counter *c){if(c->value == COUNTER_MAX) {return0; }else { c->value++;return1; }}/* decrement a counter, returns 1 if successful, 0 if decrement would cause underflow */intcounterDecrement(Counter *c){if(c->value ==0) {return0; }else { c->value--;return1; }}#ifdef TEST_MAINintmain(int argc,char **argv){ Counter *c;/* black box testing */ c = counterCreate();/* 0 */ assert(counterIsZero(c)); assert(counterIncrement(c) ==1);/* 1 */ assert(!counterIsZero(c)); assert(counterIncrement(c) ==1);/* 2 */ assert(!counterIsZero(c)); assert(counterDecrement(c) ==1);/* 1 */ assert(!counterIsZero(c)); assert(counterDecrement(c) ==1);/* 0 */ assert(counterIsZero(c)); assert(counterDecrement(c) ==0);/* 0 */ assert(counterIsZero(c)); assert(counterIncrement(c) ==1);/* 1 */ assert(!counterIsZero(c)); counterDestroy(c);/* white box testing */ c = counterCreate();/* 0 */ assert(c->value ==0); assert(counterIncrement(c) ==1);/* 1 */ assert(c->value ==1); assert(counterIncrement(c) ==1);/* 2 */ assert(c->value ==2); assert(counterDecrement(c) ==1);/* 1 */ assert(c->value ==1); assert(counterDecrement(c) ==1);/* 0 */ assert(c->value ==0); assert(counterDecrement(c) ==0);/* 0 */ assert(c->value ==0); assert(counterIncrement(c) ==1);/* 1 */ assert(c->value ==1);/* force counter value to COUNTER_MAX to test for overflow protection */ c->value = COUNTER_MAX;/* COUNTER_MAX */ assert(counterIncrement(c) ==0);/* COUNTER_MAX */ assert(c->value == COUNTER_MAX); assert(counterDecrement(c) ==1);/* COUNTER_MAX-1 */ assert(c->value == COUNTER_MAX-1); assert(counterIncrement(c) ==1);/* COUNTER_MAX */ assert(c->value == COUNTER_MAX); counterDestroy(c);return0;}#endifCC=c99CFLAGS=-g3 -Wallall: seqprinterseqprinter: main.o sequence.o$(CC)$(CFLAGS) -o$@$^test: seqprinter./seqprinter# these rules say to rebuild main.o and sequence.o if sequence.h changesmain.o: main.c sequence.hsequence.o: sequence.c sequence.hclean:$(RM) -f seqprinter *.oHere are some older notes on testing using a test harness that does some basic tricks like catching segmentation faults so that a program can keep going even if one test fails.
The module will be a stack for storing integers.
Let’s start with the interface, which we’ll put in a file calledstack.h:
/* * This is an "opaque struct"; it discourages people from looking at * the inside of our structure. The actual definiton of struct stack * is contained in stack.c. */typedefstruct stack *Stack;/* constructor and destructor */Stack stack_create(void);/* returns 0 on allocation error */void stack_destroy(Stack);/* push a new element onto the stack */void stack_push(Stack ,int new_element);/* return 1 if the stack is empty, 0 otherwise */int stack_isempty(Stack);/* remove and return top element of stack *//* returns STACK_EMPTY if stack is empty */#define STACK_EMPTY (-1)int stack_pop(Stack);Our intent is that anStack acts like a stack— we push things onto it usingstack_push, and then pull them off again in reverse order usingstack_pop. Ideally, we don’t ever pop the stack when it’s empty (which we can detect usingstack_isempty), but if we do, we havestack_pop return something well-defined.
Let’s write some test code to try this out. Because our initial stack implementation may be exceptionally bug-ridden, we’ll use a test harness that provides macros for detecting and intercepting segmentation faults and similar disasters. The various testing wrappers are defined in the filestester.h andtester.c, from thechapter on testing; you should feel free to use it for your own purposes. I’ve added line numbers in comments to all the TEST lines so we can find them again later.
#include<stdio.h>#include<setjmp.h>#include<signal.h>#include<unistd.h>#include<stdlib.h>#include"stack.h"#include"tester.h"#define STRESS_TEST_ITERATIONS (1000000)intmain(int argc,char **argv){ Stack s;int i; tester_init();/* first we need to build one */ TRY { s = stack_create(); } ENDTRY;/* 25 */ TEST_ASSERT(s !=0);/* now we'll try pushing and popping a bit */ TRY { stack_push(s,1); } ENDTRY; TRY { stack_push(s,2); } ENDTRY; TRY { stack_push(s,3); } ENDTRY;/* 32 */ TEST(stack_isempty(s),0);/* 33 */ TEST(stack_pop(s),3);/* 34 */ TEST(stack_isempty(s),0);/* 35 */ TEST(stack_pop(s),2);/* 36 */ TEST(stack_isempty(s),0);/* 37 */ TEST(stack_pop(s),1);/* 38 */ TEST(stack_isempty(s),1);/* 39 */ TEST(stack_pop(s), STACK_EMPTY);/* 40 */ TEST(stack_isempty(s),1);/* 41 */ TEST(stack_pop(s), STACK_EMPTY);/* can we still push after popping too much? */ TRY { stack_push(s,4); } ENDTRY;/* 45 */ TEST(stack_isempty(s),0);/* 46 */ TEST(stack_pop(s),4);/* 47 */ TEST(stack_isempty(s),1);/* 48 */ TEST(stack_pop(s), STACK_EMPTY);/* 49 */ TEST(stack_isempty(s),1);/* let's do some stress testing *//* we won't useTEST for this because we might get too much output */ TRY {for(i =0; i < STRESS_TEST_ITERATIONS; i++) { stack_push(s, i); }for(i =0; i < STRESS_TEST_ITERATIONS; i++) { stack_push(s,957);if(stack_pop(s) !=957) {/* 60 */ FAIL("wanted 957 but didn't get it"); abort(); } }for(i = STRESS_TEST_ITERATIONS -1; i >=0; i--) {if(stack_isempty(s)) {/* 66 */ FAIL("stack empty too early"); abort(); }if(stack_pop(s) != i) {/* 70 */ FAIL("got wrong value!"); abort(); } } } ENDTRY;/* 74 *//* 76 */ TEST(stack_isempty(s),1); TRY { stack_destroy(s); } ENDTRY; tester_report(stdout, argv[0]);return tester_result();}There is a lot of test code here. In practice, we might write just a few tests to start off with, and, to be honest, I didn’t write all of this at once. But you can never have too many tests— if nothing else, they give an immediate sense of gratification as the number of failed tests drops.
Makefile:CC=gccCFLAGS=-g3 -Wallall:test: test-stack ./test-stack @echo OK!test-stack: test-stack.o tester.o stack.o $(CC) $(CFLAGS) -o $@ $^test-stack.o: stack.h tester.hstack.o: stack.hNote that wedon’t provide a convenient shortcut for buildingtest-stack without running it. That’s because we want to run the test code every single time.
Of course, we still can’t compile anything, because we don’t have any implementation. Let’s fix that. To make it easy to write, we will try to add as little as possible to what we already have instack.h:
#include<stdlib.h>#include"stack.h"struct stack {int placeholder; };Stack stack_create(void) {return malloc(sizeof(struct stack)); }void stack_destroy(Stack s) { free(s); }void stack_push(Stack s,int elem) { ; }int stack_pop(Stack s) {return STACK_EMPTY; }int stack_isempty(Stack s) {return1; }Will this work? Of course not. There’s hardly any code! But maybe it will compile if we runmake test:
$ make testgcc -g3 -Wall -c -o test-stack.o test-stack.cgcc -g3 -Wall -c -o tester.o tester.cgcc -g3 -Wall -c -o stack.o stack.cgcc -g3 -Wall -o test-stack test-stack.o tester.o stack.o./test-stack test-stack.c:32: TEST FAILED: stack_isempty(s) -> 1 but expected 0test-stack.c:33: TEST FAILED: stack_pop(s) -> -1 but expected 3test-stack.c:34: TEST FAILED: stack_isempty(s) -> 1 but expected 0test-stack.c:35: TEST FAILED: stack_pop(s) -> -1 but expected 2test-stack.c:36: TEST FAILED: stack_isempty(s) -> 1 but expected 0test-stack.c:37: TEST FAILED: stack_pop(s) -> -1 but expected 1test-stack.c:45: TEST FAILED: stack_isempty(s) -> 1 but expected 0test-stack.c:46: TEST FAILED: stack_pop(s) -> -1 but expected 4test-stack.c:60: wanted 957 but didn't get ittest-stack.c:74: Aborted (signal 6)./test-stack: errors 8/17, signals 1, FAILs 1make[1]: *** [test] Error 8Hooray! It compiles on the first try! (Well, not really, but let’s pretend it did.) Unfortunately, it only passes any tests at all by pure dumb luck. But now we just need to get the code to pass a few more tests.
Here’s a first attempt at a stack that suffers from some artificial limits. We retain the structure of the original broken implementation, we just put a few more lines of code in and format it more expansively.
#include<stdlib.h>#include"stack.h"#define MAX_STACK_SIZE (100)struct stack {int top;int data[MAX_STACK_SIZE];};Stackstack_create(void){struct stack *s; s = malloc(sizeof(*s)); s->top =0;return s;}voidstack_destroy(Stack s){ free(s);}voidstack_push(Stack s,int elem){ s->data[(s->top)++] = elem;}intstack_pop(Stack s){return s->data[--(s->top)];}intstack_isempty(Stack s){return s->top ==0;}Let’s see what happens now:
$ make testgcc -g3 -Wall -c -o test-stack.o test-stack.cgcc -g3 -Wall -c -o tester.o tester.cgcc -g3 -Wall -c -o stack.o stack.cgcc -g3 -Wall -o test-stack test-stack.o tester.o stack.o./test-stack test-stack.c:40: TEST FAILED: stack_isempty(s) -> 0 but expected 1test-stack.c:41: TEST FAILED: stack_pop(s) -> 409 but expected -1test-stack.c:47: TEST FAILED: stack_isempty(s) -> 0 but expected 1test-stack.c:48: TEST FAILED: stack_pop(s) -> 0 but expected -1test-stack.c:49: TEST FAILED: stack_isempty(s) -> 0 but expected 1test-stack.c:74: Segmentation fault (signal 11)test-stack.c:76: TEST FAILED: stack_isempty(s) -> 0 but expected 1free(): invalid pointer 0x804b830!./test-stack: errors 6/17, signals 1, FAILs 0make[1]: *** [test] Error 6There are still errors, but we get past several initial tests before things blow up. Looking back at the line numbers intest-stack.c, we see that the first failed test is the one that checks if the stack is empty after we pop from an empty stack. The code forstack_isempty looks pretty clean, so what happened? Somewheres->top got set to a nonzero value, and the only place this can happen is insidestack_pop. Aha! There’s no check instack_pop for an empty stack, so it’s decrementings->top past 0. (Exercise: why didn’t the test ofstack_pop fail?)
If we’re lucky, fixing this problem will make the later tests happier. Let’s try a new version ofstack_pop. We’ll leave everything else the same.
intstack_pop(Stack s){if(stack_isempty(s)) {return STACK_EMPTY; }else {return s->data[--(s->top)]; }}
And now we get:
$ make testgcc -g3 -Wall -c -o test-stack.o test-stack.cgcc -g3 -Wall -c -o tester.o tester.cgcc -g3 -Wall -c -o stack.o stack.cgcc -g3 -Wall -o test-stack test-stack.o tester.o stack.o./test-stack test-stack.c:74: Segmentation fault (signal 11)test-stack.c:76: TEST FAILED: stack_isempty(s) -> 0 but expected 1./test-stack: errors 1/17, signals 1, FAILs 0make[1]: *** [test] Error 1Which is much nicer. We are still failing the stress test, but that’s not terribly surprising.
After some more tinkering, this is what I ended up with. This version uses a malloc’d data field, and realloc’s it when the stack gets too big.
#include<stdlib.h>#include"stack.h"struct stack {int top;/* first unused slot in data */int size;/* number of slots in data */int *data;/* stack contents */};#define INITIAL_STACK_SIZE (1)#define STACK_SIZE_MULTIPLIER (2)Stackstack_create(void){struct stack *s; s = malloc(sizeof(*s));if(s ==0)return0; s->top =0; s->size = INITIAL_STACK_SIZE; s->data = malloc(s->size *sizeof(*(s->data)));if(s->data ==0)return0;/* else everything is ok */return s;}voidstack_destroy(Stack s){ free(s->data); free(s);}voidstack_push(Stack s,int elem){if(s->top == s->size) {/* need more space */ s->size *= STACK_SIZE_MULTIPLIER; s->data = realloc(s->data, s->size *sizeof(*(s->data)));if(s->data ==0) { abort();/* we have no other way to signal failure :-( */ } }/* now there is enough room */ s->data[s->top++] = elem;}intstack_pop(Stack s){if(stack_isempty(s)) {return STACK_EMPTY; }else {return s->data[--(s->top)]; }}intstack_isempty(Stack s){return s->top ==0;}At last we have a version that passes all tests:
$ make testgcc -g3 -Wall -c -o test-stack.o test-stack.cgcc -g3 -Wall -c -o tester.o tester.cgcc -g3 -Wall -c -o stack.o stack.cgcc -g3 -Wall -o test-stack test-stack.o tester.o stack.o./test-stack OK!Writing a big program all at once is hard. If you can break the problem down into little problems, it becomes easier. “Test first” is a strategy not just for getting a well-tested program, but for giving you something easy to do at each step— it’s usually not too hard to write one more test, and it’s usually not too hard to get just one test working. If you can keep taking those small, easy steps, eventually you will run out of failed tests and have a working program.
/* * Test macros. * * Usage: * * #include <setjmp.h> * #include <stdio.h> * #include <signal.h> * #include <unistd.h> * * testerInit(); -- Initialize internal data structures. * testerReport(FILE *, "name"); -- Print report. * testerResult(); -- Returns # of failed tests. * * TRY { code } ENDTRY; * * Wraps code to catch seg faults, illegal instructions, etc. May not be * nested. * Prints a warning if a signal is caught. * To enforce a maximum time, set alarm before entering. * *TEST(expr, expected_value); * * Evaluates expr (which should yield an integer value) inside a TRY. * Prints a warning if evaluating expr causes a fault or returns a value * not equal to expected_value. * * TEST_ASSERT(expr) * * Equivalent toTEST(!(expr), 0) * * You can also cause your own failures with FAIL: * * TRY { * x = 1; * if(x == 2) FAIL("why is x 2?"); * } ENDTRY; * * To limit the time taken by a test, call tester_set_time_limit with * a new limit in seconds, e.g. * * tester_set_time_limit(1); * TRY { while(1); } ENDTRY; * * There is an initial default limit of 10 seconds. * If you don't want any limit, set the limit to 0. * *//* global data used by macros *//* nothing in here should be modified directly */externstruct tester_global_data { jmp_buf escape_hatch;/* jump here on surprise signals */int escape_hatch_active;/* true if escape hatch is usable */int tests;/* number of tests performed */int errors;/* number of tests failed */int signals;/* number of signals caught */int expr_value;/* expression value */int setjmp_return;/* return value from setjmp */int try_failed;/* true if last try failed */int user_fails;/* number of calls to FAIL */int time_limit;/* time limit for TRY */} TesterData;/* set up system; call this before using macros */void testerInit(void);/* prints a summary report of all errors to f, prefixed with preamble *//* If there were no errors, nothing is printed */void testerReport(FILE *f,constchar *preamble);/* returns number of errors so far. */int testerResult(void);/* set a time limit t for TRY,TEST, TEST_ASSERT etc. *//* After t seconds, an ALARM signal will interrupt the test. *//* Set t = 0 to have no time limit. *//* Default time limit is 10 seconds. */void tester_set_time_limit(int t);constchar *testerStrsignal(int);/* internal hack; don't use this *//* gruesome non-syntactic macros */#define TRY \ TesterData.try_failed = 0; \ alarm(TesterData.time_limit); \ if(((TesterData.setjmp_return = setjmp(TesterData.escape_hatch)) == 0) \ && (TesterData.escape_hatch_active = 1) /* one = is correct*/)#define ENDTRY else { \ fprintf(stderr, "%s:%d: %s (signal %d)\n", \ __FILE__, __LINE__, \ testerStrsignal(TesterData.setjmp_return), \ TesterData.setjmp_return); \ TesterData.signals++; \ TesterData.try_failed = 1; \ } \ alarm(0); \ TesterData.escape_hatch_active = 0/* another atrocity */#define TEST(expr, expected_value) \ TesterData.tests++; \ TesterData.errors++; /* guilty until proven innocent */ \ TRY { TesterData.expr_value = (expr); \ if(TesterData.expr_value != expected_value) { \ fprintf(stderr, "%s:%d: TEST FAILED: %s -> %d but expected %d\n", \ __FILE__, __LINE__, __STRING(expr), \ TesterData.expr_value, expected_value); \ } else { \ TesterData.errors--; \ } \ } \ ENDTRY; \ if(TesterData.try_failed) \ fprintf(stderr, "%s:%d: TEST FAILED: %s caught signal\n", \ __FILE__, __LINE__, __STRING(expr))#define TEST_ASSERT(expr) TEST((expr) != 0, 1)#define FAIL(msg) \ (fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, (msg)), \ TesterData.user_fails++, \ TesterData.try_failed = 1)#define _GNU_SOURCE/* get strsignal def */#include<stdio.h>#include<signal.h>#include<string.h>#include<setjmp.h>#include"tester.h"struct tester_global_data TesterData;constchar *testerStrsignal(int sig){return strsignal(sig);}staticvoidtester_sighandler(int signal){if(TesterData.escape_hatch_active) { TesterData.escape_hatch_active =0; longjmp(TesterData.escape_hatch, signal); }}voidtesterInit(void){ TesterData.escape_hatch_active =0; TesterData.tests =0; TesterData.errors =0; TesterData.signals =0; TesterData.user_fails =0; signal(SIGSEGV, tester_sighandler); signal(SIGILL, tester_sighandler); signal(SIGFPE, tester_sighandler); signal(SIGALRM, tester_sighandler); signal(SIGBUS, tester_sighandler); signal(SIGABRT, tester_sighandler);}voidtesterReport(FILE *f,constchar *preamble){if(TesterData.errors !=0 || TesterData.signals !=0) { fprintf(f,"%s: errors %d/%d, signals %d, FAILs %d\n", preamble, TesterData.errors, TesterData.tests, TesterData.signals, TesterData.user_fails); }}inttesterResult(void){return TesterData.errors;}voidtester_set_time_limit(int t){ TesterData.time_limit = t;}The fundamental principle of algorithm design was best expressed by the mathematicianGeorge Polya: “If there is a problem you can’t solve, then there is an easier problem you can solve: find it.” For computers, the situation is even better: if there is any technique to make a problem easier even by a tiny bit, then you can repeat the technique—possibly millions or even billions of times—until the problem becomes trivial.
For example, suppose we want to find the maximum element of an array ofn ints, but we are as dumb as bricks, so it doesn’t occur to us to iterate through the array keeping track of the largest value seen so far. We might instead be able to solve the problem by observing that the maximum element is either (a) the last element, or (b) the maximum of the firstn − 1 elements, depending on which is bigger. Figuring out (b) is an easier version of the original problem, so we are pretty much done once we’ve realized we can split the problem in this way. Here’s the code:
/* returns maximum of the n elements in a */intmax_element(int a[],int n){int prefix_max; assert(n >0);if(n ==1) {return a[0]; }else { prefix_max = max_element(a, n-1);if(prefix_max < a[n-1]) {return a[n-1]; }else {return prefix_max; } }}Note that we need a special case for a 1-element array, because the empty prefix of such an array has no maximum element. We alsoassert that the array contains at least one element, just to avoid mischief.
One problem with this algorithm (at least when coding in C) is that the recursion may get very deep. Fortunately, there is a straightforward way to convert the recursion to a loop. The idea is that instead of returning a value from the recursive call, we put it in a variable that gets used in the next pass through the loop. The result is
/* returns maximum of the n elements in a */intmax_element(int a[],int n){int i;/* this replaces n-1 from the recursive version */int prefix_max; assert(n >0); prefix_max = a[0];/* this is the i == 0 case */for(i =1; i < n; i++) {if(prefix_max < a[i]) { prefix_max = a[i];/* was return a[n-1] */ }/* else case becomes prefix_max = prefix_max, a noop */ }/* at the end we have to return a value for real */return prefix_max;}Algorithm design often requires both creativity and problem-specific knowledge, but there are certain common techniques that appear over and over again. The following classification is adapted from Anany Levitin,Introduction to the Design & Analysis of Algorithms, Addison-Wesley, 2003.
Some of these approaches work better than others—it is the role of algorithm analysis (and experiments with real computers) to figure out which are likely to be both correct and efficient in practice. But having all of them in your toolbox lets you try different possibilities for a given problem.
Though this classification is not completely well-defined, and is a bit arbitrary for some algorithms, it does provide a useful list of things to try in solving a problem. Here are some examples of applying the different approaches to a simple problem, the problem of finding the maximum of an array of integers.
The sorting problem asks, given as input an arrayA ofn elements in arbitrary order, to produce as output an array containing the samen elements in nondecreasing order, i.e. withA[i] ≤ A[i + 1] for alli. We can apply each of the techniques above to this problem and get a sorting algorithm (though some are not very good).
Sometimes it is convenient to consider a block ofchars as really being a block of bits. This requires using C’s bit operators to get at individual bits.
Here are some simple macros for extracting a particular bit from achar array, thought of as a large vector of bits. These assume that the bytes are stored inlittle-endian order, which means that the least significant bytes come first (seeEndianness). This may produce odd results if you feed them achar * that has been converted from a larger integer type.
#define BITS_PER_BYTE (8)/* extract the n-th bit of x */#define GET_BIT(x, n) ((((x)[(n) / BITS_PER_BYTE]) & (0x1 << ((n) % BITS_PER_BYTE))) != 0)/* set the n-th bit of x to 1 */#define SET_BIT(x, n) ((x)[(n) / BITS_PER_BYTE]) |= (0x1 << ((n) % BITS_PER_BYTE))/* set the n-th bit of x to 0 */#define RESET_BIT(x, n) ((x)[(n) / BITS_PER_BYTE]) &= ~(0x1 << ((n) % BITS_PER_BYTE))If you want to get multiple bits, use the right-shift operator to shift them over to the right end of the word and then mask with bitwise AND. For example:
#define BITS_PER_BYTE (8)/* this rather nasty expression constructs an all-ones byte */#define BYTE_MASK ((1 << BITS_PER_BYTE) - 1)/* extract the n-th byte from a word */#define GET_BYTE(x, n) (((x) >> BITS_PER_BYTE * (n)) & BYTE_MASK)/* extract n bits starting at position i from x */#define GET_BITS(x, i, j) (((x) >> (i)) & ((1 << n) - 1))/* another definition of GET_BIT */#define GET_BIT2(x, n) GET_BITS(x, n, 1)Many much more sophisticated techniques for doing bit-fiddling can be found athttp://www.jjj.de/bitwizardry/bitwizardrypage.html.
When a C program exits, all of its global variables, local variables, and heap-allocated blocks are lost. Its memory is reclaimed by the operating system, erased, and handed out to other programs. So what happens if you want to keep data around for later?
To make this problem concrete, let’s suppose we want to keep track of a hit counter for web pages. From time to time, the user will run the commandcount_hit number wherenumber is an integer value in the range 0 to 99, say. (A real application would probably be using urls, but let’s keep things as simple as possible.) We wantcount_hit to print the number of times the page with the given number has been hit, i.e. 1 the first time it is called,2 the next time, etc. Where can we store the counts so that they will survive to the next execution ofcount_hit?
The simplest solution is probably to store the data in a text file. Here’s a program that reads a filehits, increments the appropriate value, and the writes out a new version. To reduce the chances that data is lost (say ifcount_hit blows up halfway through writing the file), the new values are written to a new filehit~, which is then renamed tohit, taking the place of the previous version.
#include<stdio.h>#include<stdlib.h>#define NUM_COUNTERS (100)/* number of counters we keep track of */#define COUNTER_FILE "/tmp/hit"/* where they are stored */#define NEW_COUNTER_FILE COUNTER_FILE "~"/* note use of constant string concatenation */intmain(int argc,char **argv){int c;int i;int counts[NUM_COUNTERS];FILE *f;if(argc <2) { fprintf(stderr,"Usage: %s number\n", argv[0]); exit(1); }/* else */ c = atoi(argv[1]);if(c <0 || c >= NUM_COUNTERS) { fprintf(stderr,"Counter %d not in range 0..%d\n", c, NUM_COUNTERS -1); exit(2); } f = fopen(COUNTER_FILE,"r");if(f ==0) { perror(COUNTER_FILE); exit(3); }/* read them in */for(i =0; i < NUM_COUNTERS; i++) { fscanf(f,"%d", &counts[i]); } fclose(f); printf("%d\n", ++counts[c]);/* write them back */ f = fopen(NEW_COUNTER_FILE,"w");for(i =0; i < NUM_COUNTERS; i++) { fprintf(f,"%d\n", counts[i]); } fclose(f); rename(NEW_COUNTER_FILE, COUNTER_FILE);return0;}If you want to use this, you will need to create an initial file/tmp/hit withNUM_COUNTERS zeroes in it.
Using a simple text file like this is the easiest way to keep data around, since you can look at the file with a text editor or other tools if you want to do things to it. But it means that the program has to parse the file every time it runs. We can speed things up a little bit (and simplify the code) by storing the values in binary.
Here’s a version that stores the data as a binary file of exactlysizeof(int) * NUM_COUNTERS bytes. It uses thestdio routinesfread andfwrite to read and write the file. These are much faster than the loops in the previous program, since they can just slap the bytes directly intocounts without processing them at all.
The program also supplies and extra flagb tofopen. This is ignored on Unix-like machines but is needed on Windows machines to tell the operating system that the file contains binary data (such files are stored differently from text files on Windows).
#include<stdio.h>#include<stdlib.h>#define NUM_COUNTERS (100)/* number of counters we keep track of */#define COUNTER_FILE "/tmp/hit"/* where they are stored */#define NEW_COUNTER_FILE COUNTER_FILE "~"/* note use of constant string concatenation */intmain(int argc,char **argv){int c;int counts[NUM_COUNTERS];FILE *f;if(argc <2) { fprintf(stderr,"Usage: %s number\n", argv[0]); exit(1); }/* else */ c = atoi(argv[1]);if(c <0 || c >= NUM_COUNTERS) { fprintf(stderr,"Counter %d not in range 0..%d\n", c, NUM_COUNTERS -1); exit(2); } f = fopen(COUNTER_FILE,"rb");if(f ==0) { perror(COUNTER_FILE); exit(3); }/* read them in */ fread(counts,sizeof(*counts), NUM_COUNTERS, f); fclose(f); printf("%d\n", ++counts[c]);/* write them back */ f = fopen(NEW_COUNTER_FILE,"wb"); fwrite(counts,sizeof(*counts), NUM_COUNTERS, f); fclose(f); rename(NEW_COUNTER_FILE, COUNTER_FILE);return0;}Again, you’ll have to initialize/tmp/hit to use this; in this case, you want it to contain exactly 400 null characters. On a Linux machine you can do this with the commanddd if=/dev/zero of=/tmp/hit bs=400 count=1.
The advantage of using binary files is that reading and writing them is both simpler and faster. The disadvantages are (a) you can’t look at or update the binary data with your favorite text editor any more, and (b) the file may no longer be portable from one machine to another, if the different machines have different endianness or different values ofsizeof(int). The second problem we can deal with by converting the data to a standard word size and byte order before storing it, but then we lose some advantages of speed.
We still may run into speed problems ifNUM_COUNTERS is huge. The next program avoids rewriting the entire file just to update one value inside it. This program uses thefseek function to position the cursor inside the file. It opens the file using the"r+b" flag tofopen, which means to open an existing binary file for reading and writing.
#include<stdio.h>#include<stdlib.h>#define NUM_COUNTERS (100)/* number of counters we keep track of */#define COUNTER_FILE "/tmp/hit"/* where they are stored */intmain(int argc,char **argv){int c;int count;FILE *f;if(argc <2) { fprintf(stderr,"Usage: %s number\n", argv[0]); exit(1); }/* else */ c = atoi(argv[1]);if(c <0 || c >= NUM_COUNTERS) { fprintf(stderr,"Counter %d not in range 0..%d\n", c, NUM_COUNTERS -1); exit(2); } f = fopen(COUNTER_FILE,"r+b");if(f ==0) { perror(COUNTER_FILE); exit(3); }/* read counter */ fseek(f,sizeof(int) * c, SEEK_SET); fread(&count,sizeof(int),1, f); printf("%d\n", ++count);/* write it back */ fseek(f,sizeof(int) * c, SEEK_SET); fwrite(&count,sizeof(int),1, f); fclose(f);return0;}Note that this program is not only shorter than the last one, but it also avoids allocating thecounts array. It also is less likely to run into trouble with running out of space during writing. If we ignore issues of concurrency, this is the best we can probably do with juststdio.
We can do even better using themmap routine, available in all POSIX-compliant C libraries.POSIX, which is short forPortable Standard Unix, is supported by essentially all Unix-like operating systems and NT-based versions of Microsoft Windows. Themmap routine tells the operating system to “map” a file in the filesystem to a region in the process’s address space. Reading bytes from this region will read from the file; writing bytes to this region will write to the file (although perhaps not immediately). Even better, if more than one process callsmmap on the same file at once, they will share the memory region, so that updates made by one process will be seen immediately by the others (with some caveats having to do with how concurrent access to memory actually works on real machines).
Here is the program usingmmap:
#include<stdio.h>#include<stdlib.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<sys/mman.h>/* For mmap. I think mman is short for "memory management." */#define NUM_COUNTERS (100)/* number of counters we keep track of */#define COUNTER_FILE "/tmp/hit"/* where they are stored */#define NEW_COUNTER_FILE COUNTER_FILE "~"/* note use of constant string concatenation */intmain(int argc,char **argv){int c;int *counts;int fd;if(argc <2) { fprintf(stderr,"Usage: %s number\n", argv[0]); exit(1); }/* else */ c = atoi(argv[1]);if(c <0 || c >= NUM_COUNTERS) { fprintf(stderr,"Counter %d not in range 0..%d\n", c, NUM_COUNTERS -1); exit(2); }/* open and map the file */ fd = open(COUNTER_FILE, O_RDWR);if(fd <0) { perror(COUNTER_FILE); exit(3); } counts = mmap(0,sizeof(*counts) * NUM_COUNTERS, PROT_READ|PROT_WRITE, MAP_SHARED, fd,0);if(counts ==0) { perror(COUNTER_FILE); exit(4); } printf("%d\n", ++counts[c]);/* unmap the region and close the file just to be safe */ munmap(counts,sizeof(*counts) * NUM_COUNTERS); close(fd);return0;}Now the code for actually incrementingcounts[c] and writing it to the file is trivial. Unfortunately, we have leftstdio behind, and have to deal with low-level POSIX calls likeopen andclose to get at the file. Still, this may be the most efficient version we can do, and becomes even better if we plan to do many updates to the same file, since we can just keep the file open.
All of the solutions described so far can fail if you run two copies ofcount_hits simultaneously. Themmap solution is probably the least vulnerable to failures, as the worst that can happen is that some update is lost if the same locations is updated at exactly the same time. The other solutions can fail more spectacularly; simultaneous writes to/tmp/hit~ in the simple text file version, for example, can produce a wide variety of forms of file corruption. For a simple web page hit counter, this may not be a problem. If you are writing a back-end for a bank, you probably want something less vulnerable.
Database writers aim for a property calledACIDity from the acronymACID =Atomicity,Consistency,Isolation, andDurability. These are defined for a system in which the database is accessed viatransactions consisting of one or more operations. An example of a transaction might be++counts[c], which we can think of as consisting of two operations: readingcounts[c], and writing backcounts[c]+1.
Atomicity means that either every operation in a transaction is performed or none is. In practice, this means if the transaction fails any partial progress must be undone.
Consistency means that at the end of a transaction the database is in a “consistent” state. This may just mean that no data has been corrupted (e.g. in the text data file we have exactly 100 lines and they’re all integer counts), or it may also extend to integrity constraints enforce by the database (e.g. in a database of airline flights, the fact that flight 2937 lands at HVN at 22:34 on 12/17 implies that flight 2937 exists, has an assigned pilot, etc.).
Isolation says that two concurrent transactions can’t detect each other; the partial progress of one transaction is not visible to others until the transaction commits.
Durability means that the results of any committed transaction are permanent. In practice this means there is enough information physically written to a disk to reconstruct the transaction before the transaction finished.
How can we enforce these requirements for our hit counter? Atomicity is not hard: if I stop a transaction after a read but before the write, no one will be the wiser (although there is a possible problem if only half of my write succeeds). Consistency is enforced by thefseek andmmap solutions, since they can’t change the structure of the file. Isolation is not provided by any of our solutions, and would require some sort of locking (e.g. usingflock) to make sure that only one program uses the file at a time. Durability is enforced by not havingcount_hits return until thefclose orclose operation has succeeded (although full durability would require runningfsync ormsync to actually guarantee data was written to disk).
Though it would be possible to provide full ACIDity with enough work, this is a situation where using an existing well-debugged tool beats writing our own. Depending on what we are allowed to do to the machine our program is running on, we have many options for getting much better handling of concurrency. Some standard tools we could use are:
Congratulations! You now know everything there is to know about programming in C. Now what do you do?
My recommendation would be the following: learn C++, since you know 75% of it already, and you will be able to escape from some (but not all) of the annoying limitations of C. And learn a scripting language you can be comfortable with, for writing programs quickly where performance isn’t the main requirement.
In this class, we had to work around fundamental limitations in C on several occasions.
malloc will be passed tofree. Not only does this create many possibilities for error, but it also means that certain kinds of data structures in which a single component of the data structure is pointed to by an unpredictable number of other components are difficult to write in C, since it’s hard to tell when it is safe to free a component. Garbage-collected languages avoid all of these problems at a slight cost in performance. Though there exists a garbage collector for C/C++http://www.hboehm.info/gc/, it isn’t 100% portable and may not work as well as a built-in collector.void * and function pointers as inqsort, or various nasty hacks where code is automatically generated with type names filled in from a base template (seeMacrosMacros.html) again). Most modern programming languages have some sort of support for polymorphism, allowing you to write, for example, a generic sorting routine without resorting tovoid *-like departures from the type system.eat_leftovers function exported fromleftovers.c doesn’t conflict with youreat_leftovers function incannibalism.c. A mediocre solution is to use longer names:leftovers_eat_leftovers vscannibalism_eat_leftovers, and one can also play games with function pointers and globalstruct variables to allow something likeleftovers.eat_leftovers vscannibalism.eat_leftovers. Most modern programming languages provide an explicit package or namespace mechanism to allow the programmer to control who sees what names where.On the above list, C++ fixes everything except the missing garbage collector. If you want to learn C++, you should get a copy ofThe C++ Programming Language, by Bjarne Stroustrup, which is the definitive reference manual. But you can get a taste of it from several on-line tutorials:
C syntax has become the default for new programming languages targeted at a general audience. Some noteworthy examples of C-like languages areJava (used in Android),Objective-C (used in OSX and iOS), andC# (used in Windows).
Each of these fix some of the misfeatures of C (including the lack of a garbage collector and bounds checks on arrays) while retaining much of the flavor of C. Which to choose probably depends on what platform you are interested in developing for.
Much current programming is done in so-calledscripting languages likePython,Perl,PHP,JavaScript,Visual Basic,Tcl, etc. These are generally interpreted languages similar to Lisp or Scheme under the hood, with dynamic typing (type information is carried along with values, so type errors are detected only at runtime but polymorphism is provided automatically), garbage collectors, and support for many advanced programming features like objects and anonymous functions. What distinguishes scripting languages from the Lisp-like languages is that the syntax is generally more accessible to newcomers and the language runtime usually comes with very large libraries providing built-in tools for doing practical programming tasks like parsing odd input formats and interfacing to databases and network services. The result is that common programming tasks can be implemented using very few lines of code, at a cost in performance that ranges from slight to horrendous depending on what you are doing.
Let’s look at an example in two common scripting languages, Perl and Python.
Here are some solutions to an old assignment, which find all the palindromes onstdin and report the first non-matching character for any non-palindrome.
The original C version looks like this:
#include<stdio.h>#include<stdlib.h>#include<string.h>/* Palindrome detector. * * For each line of the input, prints PALINDROME if it is a palindrome * or the index of the first non-matching character otherwise. * * Note: does not handle lines containing nulls. *//* read a line of text from stdin * and return it (without terminating newline) as a freshly-malloc'd block. * Caller is responsible for freeing this block. * Returns 0 on error or EOF. */char *getLine(void){char *line;/* line buffer */int n;/* characters read */int size;/* size of line buffer */int c; size =1; line = malloc(size);if(line ==0)return0; n =0;while((c = getchar()) !='\n' && c != EOF) {while(n >= size -1) { size *=2; line = realloc(line, size);if(line ==0)return0;}line[n++] = c; }if(c == EOF && n ==0) {/* got nothing */free(line);return0; }else {line[n++] = '\0';return line; }}#define IS_PALINDROME (-1)/* returns IS_PALINDROME if s is a palindrome, * or index of first unmatched character otherwise. */inttestPalindrome(constchar *s){int n;/* length of s */int i; n = strlen(s);/* we only have to check up to floor(n/2) */for(i =0; i < n/2; i++) {if(s[i] != s[n-1-i]) {return i;} }/* else */return IS_PALINDROME;}intmain(int argc,char **argv){char *line;int mismatch;while((line = getLine()) !=0) {mismatch = testPalindrome(line);if(mismatch == IS_PALINDROME) { puts("PALINDROME");}else { printf("%d\n", mismatch);}free(line); }return0;}This version is written in Perl (http://www.perl.org):
#!/usr/bin/perl# For each line in stdin, print PALINDROME if it is a palindrome, or index of# the first non-matching character otherwise.while(<>) {chomp;# remove trailing newlineif($_eqreverse$_) {print"PALINDROME\n"; }else {for$i (0..length($_) -1) {if(substr($_,$i,1)nesubstr($_,length($_) -$i -1,1)) {print$i,"\n";last; } } }}The things to notice about Perl is that the syntax is deliberately very close to C (with some idiosyncratic extensions like putting$ on the front of all variable names), and that common tasks like reading all input lines get hidden inside default constructions likewhile(<>) and the$_ variable that functions with no arguments likechomp operate on by default. This can allow for very compact but sometimes very incomprehensible code.
Here’s a version in Python (https://www.python.org/):
#!/usr/bin/python"""For each line in stdin, print PALINDROME if it is a palindrome, or index ofthe first non-matching character otherwise."""import sysfor linein sys.stdin: line= line.rstrip('\n')# remove trailing newlineif line== line[::-1]:print("PALINDROME")else: mismatches= [ ifor iinrange(len(line))if line[i]!= line[-(i+1)] ]print(min(mismatches))Here the syntax is a little more alien if you are used to C: Python doesn’t use curly braces for block structure, using indentation instead. The code above uses some other odd features of the language, such as the ability to take “slices” of sequence variables like strings (the expressionline[::-1] means "take all elements ofline starting from the obvious default starting point (the empty string before the first colon) to the obvious default ending point (the empty string before the second colon) stepping backwards one character at a time (the-1)), a feature the language adopted from array-processing languages likeMatLab; and the ability to dolist comprehensions (the large expression assigned tomismatches), a feature that Python adopted fromHaskell and that Haskell adopted from set theory.
What these gain in short code length they lose in speed; run times on/usr/share/dict/words in the Zoo are
| C | 0.107s |
| Perl | 0.580s |
| Python | 2.052s |
Note that for Perl and Python some of the cost is the time to start the interpreter and parse the script, but factors of 10–100 are not unusual slowdowns when moving from C to a scripting language. The selling point of these languages is that in many applications run time is not as critical as ease and speed of implementation.
As an even shorter example, if you just want to print all the palindromes in a file, you can do that from the command line in one line of Perl, e.g:
$ perl -ne 'chomp; print $_, "\n" if($_ eq reverse $_)' < /usr/share/dict/wordsAssignments are generally due Thursdays at 23:59 Eastern US time (the first four assignments were due at an earlier time).
The purpose of this assignment is to make sure you can test and submit assignments in the Zoo. The assignment itself is not intended to be very hard but might require a bit of thought.
Make sure that you have an account on the Zoo. This should have been created automatically when you registered for the class, but if not, you will need to contact ITS atcs.support@yale.edu. You will also need a course directory in/c/cs223/class. This should also have been created automatically, but if you have a Zoo account but do not have a course directory, you can create one using the commandsudo register cs223 in the Zoo.
You do not need to develop your solution on the Zoo, but you will need to turn it in there, and it will be tested using the compiler on the Zoo. So it’s best to make sure you have access to the Zoo as soon as possible.
For this assignment, you are to write a programhello.c that prints the string"Hello, world!", followed by a newline character, tostdout. However, because the character'l' is easily confused with the characters'1' and'|', and the character'o' is easily confused with the character'0', your hypothetical employer has forbidden any use of the characters'l','L','1','|','o','O', or'0' in source files, except in lines starting with#include. Your task is to write a program that emits the standard C greeting despite this rule. Your program should also return the standard success code0 frommain to indicate that it completed its task successfully.
Submit your assignment using the command:
/c/cs223/bin/submit 1 hello.cYou can test that your submitted program compiles and passes a few basic tests using the command:
/c/cs223/bin/testit 1 publicThis runs the test script in/c/cs223/Hwk1/test.public on the submission directory. You can also run the same script by hand on unsubmitted code in your current working directory using
/c/cs223/Hwk1/test.publicCurrently, this script awards up to 100 points according to the following rubric:
hello.c existshello.c compiles without warningshello.c compiles at allhello.c outputs the correct outputhello.c satisfies the forbidden characters ruleThis may or may not be the final rubric used to grade this assignment, but you should assume that it is always a good idea to submit something, even if it isn’t perfect.
You can submit your assignment more than once, but any late penalties will be assessed based on the last submission. The command
/c/cs223/bin/protect 1 hello.cwill protect your assignment against future modification. This is highly recommended to avoid accidentally overwriting your submission and incurring unwanted late penalties. Replaceprotect withunprotect if you need to re-enable changes.
For more details about thesubmit script and its capabilities, seehere.
There are many ways to do this, most of which involve replacing the forbidden characters and numbers with an appropriate arithmetic expression. Here is one possible approach.
#include<stdio.h>// Print standard C greeting despite restricted characters.intmain(int argc,char **argv){// The names are a Caesar cipher, shifted upint m =2*54;int p =3*37; printf("He%c%c%c, w%cr%cd!\n", m, m, p, p, m);// Advertise success!// (Even easier: It is permitted that we// just skip having a return statement// in main starting with C99, which has the// same effect.)return2-2;}For this assignment, you are to write a programgarble that introduces transposition errors that are just frequent enough to be annoying into an input provided onstdin, sending the results tostdout.
Your program should process the characters in the input in order. When it encounters two consecutive characters that are (a) both lowercase and (b) have numerical ASCII encodings that have the same remainders mod 7, it should swap these characters. All other characters should be passed through intact.
Rule (b) means that most lowercase characters willnot be swapped. For example, all of the charactersabcde have different remainders mod 7, so for an input
aababcabcdabcdethe corresponding output should be
aababcabcdabcdeOn the other hand,'a' (ASCII code 97) and'h' (ASCII code 104) have the same remainder 6 when divided by 7. So on input
aahahaahahahahathe output should be
ahahaahahahahaaNote that once a pair of characters has been swapped, those characters don’t participate in further swaps.
Non-letters and letters that are not lowercase are never swapped, and separate otherwise-swappable pairs. So
Haha, thou knave, I say haha!becomes
Hhaa, tohu knvae, I say ahah!One check to see if you are applying the rules correctly is that running an input through your program twice should restore the original input.
The easiest way to recognize a lowercase letter is to use theislower macro. You will need to put the line#include <ctype.h> near the top of your program to get access to this macro. Typical usage:
if(islower(c)) { printf("%c is a lowercase letter\n", c); }else { printf("%c is not a lowercase letter\n", c); }A subtlety is thatislower looks at the C locale settings to decide which alphabet it should be using. For example,'ö' is a perfectly cromulent lowercase letter if your locale is set to Swedish. The test script used for this assignment will force the locale toC, representing the default US English assumptions made in 1970’s-era C, which may cause your program to behave differently from when you run it by hand if your locale is set to something else. If you suspect this is an issue, you can force the locale to C on the command line using
LC_ALL=C ./garble < someInputFile > someOutputFileAny sequence of bytes is a possible input to your program, so you should not assume that input characters are restricted to the range 0..127 used for ASCII. One particularly troublesome character is'ÿ', which has numerical value 255 in the common ISO Latin 1 encoding. This will look very much likeEOF (usually -1) if you assign both to an 8-bitchar type. If you do the usual thing and store the output ofgetchar in anint, this shouldn’t be a problem.
Some sample inputs and outputs can be found in the directory/c/cs223/Hwk2/testFiles in the Zoo, though you should also feel free to create test inputs of your own. Note that some of the sample files contain non-printing characters that may have odd effects if you send them to your screen. A simple way to test if your program produces the same output as the sample output is to usecmp, for example:
$ ./garble < test.in > tmp$ cmp tmp test.outIftmp andtest.out contain the same characters,cmp will say nothing. Otherwise it will tell you the first position where they differ.
If you want to examine the characters in a binary file, you can useod, as in
$ echo hi > hi.txt$ od -t x1 -t c hi.txt0000000 68 69 0a h i \n0000003Two adjacent identical lowercase characters have the same remainder mod 7, and so should be swapped. This swap will not be visible in the output (the characters are identical!) but may block other possible swaps. For example, the inputaah passes through unchanged: since the two'a' characters are swapped, the second'a' can’t participate in a swap with the'h'.
Submit your assignment using the command:
/c/cs223/bin/submit 2 garble.cYou can test that your submitted program compiles (and passes a few basic tests) using the command:
/c/cs223/bin/testit 2 publicThis runs the test script in/c/cs223/Hwk2/test.public on the submission directory. You can also run the same script by hand on your unsubmitted code using
/c/cs223/Hwk2/test.publicThe inputs and outputs this script expects can be found in the directory/c/cs223/Hwk2/testFiles/, in case you want to look more closely at what each test is doing.
The unsympathetic robo-grading script used to grade this assignment may or may not use the same tests as this command, so you should make sure your program works on other inputs as well. You may also want to look at thestyle grading checklist to see that you haven’t committed any gross atrocities against readability, in case a human being should happen to look at your code.
You can submit your assignment more than once, but any late penalties will be assessed based on the last submission. The command
/c/cs223/bin/protect 2 garble.cwill protect your assignment against future modification. This is highly recommended to avoid accidentally overwriting your submission and incurring unwanted late penalties. Replaceprotect withunprotect if you need to re-enable changes.
For more details about thesubmit script and its capabilities, seehere.
// CPSC 223 HW2 sample solution//// Introduce occasional transposition errors.//// The rule is that we swap each pair of adjacent// lower case letters, provided their ASCII codes// are equivalent mod 7.#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include<ctype.h>#define MODULUS (7)intmain(int argc,char **argv){if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; }// previous input character, or EOF if there is noneint previous = EOF;// current input characterint current;while((current = getchar()) != EOF) {if(previous == EOF) {// don't do anything with this character yet previous = current; }elseif( islower(previous) && islower(current) && (previous % MODULUS == current % MODULUS)) {// swap putchar(current); putchar(previous);// reset to starting state previous = EOF; }else {// don't swap, just flush previous and update putchar(previous); previous = current; } }// make sure we output previous if we haven't alreadyif(previous != EOF) { putchar(previous); }return0;}Thedecimal.h header file below defines an interface for three functionsdecimalAdd,decimalSubtract, anddecimalPrint for working with binary-coded decimal values stored in arrays ofchars, where eachchar contains the numeric value of one of the digits, stored in least-significant-digit-first (“little endian”) order.
Your task is to provide a filedecimal.c that implements these functions so that their behavior matches their description in the comments indecimal.h. The arithmetic part of this implementation is likely to be straightforward, but you will need to pay attention to proper handling of errors.
// Functions for doing arithmetic on// arbitrary-precision decimal numbers.//// Each number is represented as an array// of chars, one per digit,// with the least significant digit first.//// For example, 123 would be stored in a[]// with a[0] == 3, a[1] == 2, and a[2] == 1.// Error codes#define DECIMAL_OK (0)// returned if operation succeeds#define DECIMAL_OVERFLOW (1)// returned if operation would overflow#define DECIMAL_BAD_DIGIT (2)// returned for digits outside 0..9// Used to represent numbers with bad digits in the output.#define DECIMAL_BAD_OUTPUT ("BAD")// Base for arithmetic.// This will not change, but is provided for convenience.#define DECIMAL_BASE (10)// Adds addend to augend,// leaving result in augend.// Argument precision gives size// of both augend and addend.//// Returns DECIMAL_OK with no errors,// DECIMAL_OVERFLOW if sum would overflow,// DECIMAL_BAD_DIGIT if either argument// includes a digit outside the range 0..9.//// If an error occurs, augend should not be changed.int decimalAdd(size_t precision,char augend[],constchar addend[]);// Subtracts subtrahend from minuend,// leaving result in minuend.// Argument precision gives size of both// minuend and subtrahend.//// Returns DECIMAL_OK with no errors,// DECIMAL_OVERFLOW if result would be negative,// DECIMAL_BAD_DIGIT if either argument// includes a digit outside the range 0..9.//// If an error occurs, minuend should not be changed.int decimalSubtract(size_t precision,char minuend[],constchar subtrahend[]);// Print number with given precision to stdout, omitting leading zeros.// If number contains bad digits, print the string DECIMAL_BAD_OUTPUT.void decimalPrint(size_t precision,constchar number[]);We have provided a test harnesstestDecimal.c that can be compiled together withdecimal.c to produce a runnable test program. When running this program, the first two arguments specify the precision and the test to run.
Some tests take additional arguments that provide inputs to particular functions. Theprint test takes one argument and callstestPrint on it. Theadd andsub tests take two arguments and will print the return value ofdecimalAdd ordecimalSubtract, as well as callingdecimalPrint on the two arrays after executingdecimalAdd ordecimalSubtract. TheaddHuge andsubHuge tests take no arguments, but instead generate pseudorandom inputs using the precision as a seed. Otherwise their output is similar to theadd andsub tests.
Here is an example of runningtestDecimal on some simple test cases:
$ ./testDecimal 12 empty$ ./testDecimal 12 print 1234512345$ ./testDecimal 12 print MelvilleBAD$ ./testDecimal 12 add 12345 98765011111098765$ ./testDecimal 5 add 12345 9876511234598765./testDecimal: test add failed$ ./testDecimal 12 add 12345 Melville212345BAD./testDecimal: test add failed$ ./testDecimal 12 sub 12345 9876511234598765./testDecimal: test sub failed$ ./testDecimal 12 sub 98765 1234508642012345$ ./testDecimal 50 addHuge04024571150190664651929277616310163294032371097098922647134177517598577324389047941968387115159664553$ ./testDecimal 50 subHuge11759857732438904794196838711515966455320855130643622647134177517598577324389047941968387115159664553./testDecimal: test subHuge failedYou can also run the public test script in/c/cs223/Hwk3/test.public, which will grabdecimal.c from your current working directory, or run/c/cs223/bin/testit 3 public to run the same test script on your submitted solution.
Use/c/cs223/bin/submit 3 decimal.c to submit your assignment. You do not need to submitdecimal.h ortestDecimal.c; these will be supplied by the test script.
There is some similarity between this assignment andAssignment 3 from 2018. If you wanted to, you could perhaps solve this assignment using code from the sample solutions to that assignment, as long as you were careful to provide proper attribution. But there are enough differences between the interfaces in the two assignments that it will probably be much easier to solve this assignment from scratch.
Your code is not responsible for detecting if the value ofprecision provided to any of the functionsdecimalAdd,decimalSubtract, ordecimalPrint matches the actual size of the array arguments. This is an application of the fundamental principle thatwith no power comes no responsibility26. Because the array arguments are just raw addresses that could point to arbitrary locations in memory, there is no mechanism in C to determine how those address were generated and whether they map to an actual array of the appropriate size or not. So you can operate under the assumption thatprecision is correct, and if it isn’t, it’s not your problem.
Suppressing leading zeros does not apply to the number 0 itself, which should be printed as0. This is also the correct output fordecimalPrint whenprecision is 0.
The description in the comment for whatdecimalPrint should output for numbers containing bad digits is confusing. In this case,decimalPrint should print thevalue of the macroDECIMAL_BAD_OUTPUT defined indecimal.h, which is currently"BAD", not the string"DECIMAL_BAD_OUTPUT" itself. It is also a good idea to make sure that your code will continue to work if the definition indecimal.h changes.
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include"decimal.h"#define MAX_DIGIT (DECIMAL_BASE - 1)// Return 1 if number includes a digit// outside range 0..9. Return 0 otherwise.staticinthasBadDigit(size_t precision,constchar number[]){for(size_t i =0; i < precision; i++) {if(number[i] <0 || number[i] > MAX_DIGIT) {return1; } }// elsereturn0;}intdecimalAdd(size_t precision,char augend[],constchar addend[]){// check for bad digitsif(hasBadDigit(precision, augend) || hasBadDigit(precision, addend)) {return DECIMAL_BAD_DIGIT; }// do trial addition to check for overflowint carry =0;for(size_t i =0; i < precision; i++) { carry = (addend[i] + augend[i] + carry) / DECIMAL_BASE; }if(carry !=0) {return DECIMAL_OVERFLOW; }// no overflow, do the real addition carry =0;int digitSum;for(size_t i =0; i < precision; i++) { digitSum = augend[i] + addend[i] + carry; augend[i] = digitSum % DECIMAL_BASE; carry = digitSum / DECIMAL_BASE; }return DECIMAL_OK;}intdecimalSubtract(size_t precision,char minuend[],constchar subtrahend[]){// check for bad digitsif(hasBadDigit(precision, minuend) || hasBadDigit(precision, subtrahend)) {return DECIMAL_BAD_DIGIT; }// do trial subtraction to check for underflow// carry is 0 if no carry, 1 if we carried a -1int carry =0;for(size_t i =0; i < precision; i++) { carry = (minuend[i] - subtrahend[i] - carry) <0; }if(carry) {return DECIMAL_OVERFLOW; }// do real subtraction carry =0;int digitDifference;for(size_t i =0; i < precision; i++) { digitDifference = minuend[i] - subtrahend[i] - carry;// The extra parens tell gcc that we really// do want an assignment here.if((carry = (digitDifference <0))) { digitDifference += DECIMAL_BASE; } minuend[i] = digitDifference; }return DECIMAL_OK;}voiddecimalPrint(size_t precision,constchar number[]){if(precision ==0) {// annoying corner case, we'll just handle it here printf("0"); }elseif(hasBadDigit(precision, number)) { printf(DECIMAL_BAD_OUTPUT); }else {size_t i;// used in both loops, so declared outside// skip leading zerosfor(i = precision -1; number[i] ==0 && i >0; i--);// because i is unsigned, we can't test i <= 0;// instead, we detect when it wraps aroundfor(; i < precision; i--) { putchar('0' + number[i]); } }}Thesplit.h header file below defines an interface for three functionsjoin,split, andfreeSplit.
Thesplit function takes as arguments a null-terminated string and a separator character (represented as anint), returning an array of strings consisting of the substrings obtained by splitting the input string using the given separator character, followed by a null pointer to mark the end of the array. This can be used, for example, to split up a path name into its components: callingsplit("/c/cs223/bin/submit", '/') should return a six-element array of pointers to the strings"","c","cs223","bin","submit", followed by a null pointer (0). Note that consecutive occurrences of the separator may produce empty strings in the array:split("////", '/') should also return a six-element array, in which the last element is 0 and the rest of the elements are empty strings"".
Thejoin function is the inverse of split: given an array returned by split, or an array with similar structure (likeargv), it concatenates all the strings in the array together into a new string using the given separator in between each consecutive pair. In the special case where the separator is the null character'\0', the strings are concatenated together with no character between them.
Each of these functions will need to usemalloc to obtain space to store their return values. ThefreeSplit function should free any data allocated bysplit. A simple call tofree should free any data allocated byjoin.
Your task is to provide a filesplit.c that implements all three functions insplit.h.
// Split and join operations for strings// Given an array of pointers to strings,// terminated by a null pointer,// concatenate them separated by character c,// or no character if c is '\0'.// Return the result as a malloc'd string.//// Examples://// char *a[] = { "ab", "cd", "efg", 0 };// join(a, ':') returns "ab:cd:efg"// join(a, '\0') returns "abcdefg"//char *join(char **a,int c);// Reverse of join: given a string s containing// separators c, construct an array of pointers// to copies of substrings of s separated by c.// A null pointer is used to mark the end of this array.//// If c is null, a copy of the entire string will be// returned as the only substring.//// Return value may include malloc'd data that can// be freed with freeSplit if not modified.//// Examples://// split("a:b:c", ':') returns {"a", "b", "c", 0}// split("a::bc", ':') returns {"a", "", "bc", 0}// split("a::bc", '\0') returns {"a::bc", 0}//// (The initializer syntax in the return values// is for illustration only.)char **split(constchar *s,int c);// Free all space used by a, assuming a// was returned by split() and not subsequently// modified.void freeSplit(char **a);We have provided a test harnesstestSplit.c that produces a runnable test program when compiled withgcc -g3 -Wall -o testSplit split.c testSplit.c. Run./testSplit with no arguments to get a list of options.
Use/c/cs223/bin/submit 4 split.c to submit your assignment. You do not need to submitsplit.h ortestSplit.c; these will be supplied by the test script.
Thejoin routine takes achar ** as its first argument, even though it should not modify this argument in any way. This allowsargv to be passed in without any additional effort, but also allows a badly-writtenjoin routine to accidentally modify its argument (don’t do this). Similarly, thesplit routine returns achar ** for consistency withjoin, but modifying this return value might lead to trouble later (particularly infreeSplit). In principle, both of these types could be replaced with the rather baroque typeconst char * const * to indicate that neither the pointers in the array nor the strings they point to should be modified, but this might require a lot of explicit casts insidesplit andfreeSplit to add and remove theconst qualifiers. Instead, we will rely on programmer discipline and hope for the best.
You may find it helpful to use functions declared instring.h in your implementation. These are provided by the standard C library and do not require any special compile-time arguments to use. Typeman string on the command line for documentation.
Some of the tests in the public test script usevalgrind to detect storage leaks, buffer overflows, and other pointer-related errors. The script/c/cs223/bin/vg can be used to run your program with the same options as these tests, as in/c/cs223/bin/vg ./testSplit arg1 arg2 .... Ifvg produces no output, it means thatvalgrind is happy. If errors occur, you will get more informative error messages if you rangcc with debugging information turned on (the-g3 flag).
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include<string.h>#include<ctype.h>#include"split.h"// Join strings in a with separator c,// or no separator if c == '\0'.char *join(char **a,int c){// calculate total length// not including null terminator or separators,// as well as number of stringssize_t totalLength =0;size_t count =0;for(size_t i =0; a[i] !=0; i++) { totalLength += strlen(a[i]); count++; }// add separators (if not null) and null terminatorif(c != '\0') { totalLength += count -1; } totalLength++;// allocate spacechar *output = malloc(totalLength); assert(output);// useless on Linux, but a good habit// where to insert next stringchar *tail = output;// we use i+1 < count here because last one is special// we don't use i < count - 1 in case count is 0for(size_t i =0; i+1 < count; i++) {// add string strcpy(tail, a[i]); tail += strlen(a[i]);if(c != '\0') { *tail++ = c; } }// copy last string if anyif(count >0) { strcpy(tail, a[count-1]); }else {// enforce null terminator *output = '\0'; }return output;}// Inverse of join.char **split(constchar *s,int c){// First figure out how many pointers we need.// This will be equal to the number of occurrences// of c, plus 2 for the last pointer and the null// terminator.size_t count =0;for(constchar *p = s; *p; p++) {if(*p == c) { count++; } }size_t n = count+1;// number of strings in split// Instead of allocating separate blocks for each// substring, make a single copy of string and then replace// any c characters with nulls.//// If we were really getting carried away, we could just// allocate one block for both the pointers and the// strings, but this would require more pointer hackery.// It's easier to just use calloc and strdup.char **pointers = calloc(n+1,sizeof(char *));char *data = strdup(s);size_t top =0;// first unused element in pointerschar *tail = data;// start of unprocessed part of data// Process data until we hit the null terminator.// We will escape in the middle.for(;;) { pointers[top++] = tail;// look for next copy of cchar *next = strchr(tail, c);if(c == '\0' || next ==0) {// there is no next copy of c, we are donebreak; }else {// zero out *next and skip past it *next = '\0'; tail = next +1; } } assert(top == n);// if this isn't true, we made a mistake pointers[top] =0;return pointers;}// free data structure returned by splitvoidfreeSplit(char **pointers){ free(pointers[0]);// zeroth entry points to data block free(pointers);}You may recall thatcomplex numbers are numbers of the forma + bi, wherei is defined by the rulei2 = − 1. Complex numbers are afield, meaning they support the usual arithmetic operations of addition, subtraction, multiplication, and division (by any number that is not 0). The C language provides built-in support for complex numbers, although just as C’s floating-point numbers are only approximations to actual real numbers, C’s complex numbers are only approximation to actual complex numbers.
Hypercomplex numbers are a generalization of complex numbers that add one or morei-like values, along with some equations defining how these new values relate back to the underlying reals. For this assignment, we will consider a special case of hypercomplex numbers, where all our numbers are of the forma + bu, whereu is a new number that satisfiesu2 = c for some constant real valuec. Depending on what value we choose forc, we may get various complex-like number systems, like the complex numbers themselves (c = − 1), thesplit-complex numbers (c = 1), or thedual numbers (c = 0).
Most arithmetic on numbers of this form is straightforward. Addition and subtraction can be done componentwise without knowing the value ofu2:(a + bu) + (c + du) = (a + c) + (b + d)u, and similarly for subtraction. For multiplication, you can first expand(a + bu)(c + du) asac + bcu + adu + bdu2 and then use the value foru2 to get rid of theu2 term.
Division is more complicated, but can often be carried out using the same technique used to divide complex numbers. Given a fraction(a + bu)/(c + du), multiplying both numerator and denominator by theconjugatec − du of the denominator gives an expression with denominator(c + du)(c − du) = c2 − d2u2, which turns into a real number when we replaceu2 with its value. We can then divide each component of the numerator by this value to obtain a hypercomplex number again. A complication arises ifc2 − d2u2 = 0; in this case we are not responsible for returning a sensible answer.
Your task will be to implement a library that allows common arithmetic operations in any of these number systems, where the coordinatesa andb are stored asdoubles. Each number is stored as astruct h, the fields of which you will be able to choose. The library should supply all of the routines listed in this template header file:
// Two-dimensional hypercomplex numbers.//// Each number is of the form a + b*u, where// u*u == a for some real number a.//// Examples://// If u*u == -1, we get the complex numbers.// If u*u == 0, we get the dual numbers.// If u*u == 1, we get the split numbers.//// Since we can't compute with reals, we will represent// coordinates as doubles.// User is politely requested not to access fields// of this struct directly; use hPack and hUnpack.struct h {//### INSERT YOUR DEFINITION HERE###};// Construct a hypercomplex number a+b*u from the given coordinates.// Argument u2 gives the value of u*u.// The effect of operations on numbers with different values of u2// is undefined.struct h hPack(double a,double b,double u2);// Return the coordinates of a hypercomplex number// through the given pointers.void hUnpack(struct h z,double *a,double *b);// Return z1 + z2.struct h hAdd(struct h z1,struct h z2);// Return z1 - z2.struct h hSubtract(struct h z1,struct h z2);// Return z1 * z2.struct h hMultiply(struct h z1,struct h z2);// Given z == a + b*u, return its conjugate a - b*u.struct h hConjugate(struct h z);// Return q such that q*z2 == z1.// If z2 is a zero divisor, behavior is undefined.struct h hDivide(struct h z1,struct h z2);You will need to modify this template to produce a new filehypercomplex.h that includes a definition ofstruct h, which is currently empty in the template file. Other than filling in this definition, you should not change anything significant inhypercomplex.h. You will also need to supply a filehypercomplex.c that implements all of the functions declared inhypercomplex.h.
The value ofu2 used for computation is supplied when creating a hypercomplex number using thehPack routine. For operations likehAdd orhMultiply that take multiple hypercomplex arguments, you may assume that this value is the same for both arguments.
We have provided a test programcalculator.c that can be compiled together withhypercomplex.c to produce a runnableRPN calculator for hypercomplex numbers. The input commands accepted by this program are described in the comments; very briefly,= followed by two floating-point values will push a number onto the stack, the usual arithmetic operations will perform the operation on the top two stack elements, andp will print the top stack element (without removing it). The value ofu2 is provided on the command line when running this program.
Here is a typical session running this program on the terminal that does some computation with standard complex numbers (u2 = − 1):
$ ./calculator -1.0=1 1=1 2/p # divide 1+1i by 1+2i and print result0.6 -0.2=1 2*p # multiply by 1+2i again and see if we get 1+1i1 1=1 1-p # check for round-off errors0 2.22045e-16And here is the same computation with dual numbers (u2 = 0):
$ ./calculator 0.0=1 1=1 2/p # divide 1+1eps by 1+2eps and print result1 -1=1 2*p # multiply by 1+2eps again and see if we get 1+1eps1 1=1 1-p # check for round-off errors0 0Submit your assignment as usual with the command/c/cs223/bin/submit 5 hypercomplex.h hypercomplex.c. You do not need to submitcalculator.c.
You can test your submitted assignment using the public test script with/c/cs223/bin/testit 5 public. You can also run the public test script on files in your current directory with/c/cs223/Hwk5/test.public.
// Two-dimensional hypercomplex numbers.//// Each number is of the form a + b*u, where// u*u == a for some real number a.//// Examples://// If u*u == -1, we get the complex numbers.// If u*u == 0, we get the dual numbers.// If u*u == 1, we get the split numbers.//// Since we can't compute with reals, we will represent// coordinates as doubles.// User is politely requested not to access fields// of this struct directly; use hPack and hUnpack.struct h {double a;// real componentdouble b;// u componentdouble u2;// value of u*u};// Construct a hypercomplex number a+b*u from the given coordinates.// Argument u2 gives the value of u*u.// The effect of operations on numbers with different values of u2// is undefined.struct h hPack(double a,double b,double u2);// Return the coordinates of a hypercomplex number// through the given pointers.void hUnpack(struct h z,double *a,double *b);// Return z1 + z2.struct h hAdd(struct h z1,struct h z2);// Return z1 - z2.struct h hSubtract(struct h z1,struct h z2);// Return z1 * z2.struct h hMultiply(struct h z1,struct h z2);// Given z == a + b*u, return its conjugate a - b*u.struct h hConjugate(struct h z);// Return q such that q*z2 == z1.// If z2 is a zero divisor, behavior is undefined.struct h hDivide(struct h z1,struct h z2);#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include"hypercomplex.h"struct hhPack(double a,double b,double u2){struct h z = { a, b, u2 };return z;}voidhUnpack(struct h z,double *a,double *b){ *a = z.a; *b = z.b;}struct hhAdd(struct h z1,struct h z2){ assert(z1.u2 == z2.u2);struct h z = { z1.a + z2.a, z1.b + z2.b, z1.u2 };return z;}struct hhSubtract(struct h z1,struct h z2){ assert(z1.u2 == z2.u2);struct h z = { z1.a - z2.a, z1.b - z2.b, z1.u2 };return z;}// multiple a hypercomplex by a realstaticstruct hhMultiplyScalar(struct h z,double d){struct h zd = { z.a * d, z.b * d, z.u2 };return zd;}struct hhMultiply(struct h z1,struct h z2){ assert(z1.u2 == z2.u2);struct h z = { z1.a * z2.a + z1.b * z2.b * z1.u2, z1.b * z2.a + z1.a * z2.b, z1.u2 };return z;}struct hhConjugate(struct h z){struct h zbar = { z.a, -z.b, z.u2 };return zbar;}struct hhDivide(struct h z1,struct h z2){ assert(z1.u2 == z2.u2);// We do the usual trick of multiplying numerator// and denominator by the conjugate of the denominator.struct h z1c = hMultiply(z1, hConjugate(z2));struct h z2c = hMultiply(z2, hConjugate(z2));// Since z2c = (a + b u) (a - b u) = a*a - b*b*u2// has only a real component, we can just divide this out.return hMultiplyScalar(z1c,1.0 / z2c.a);}For this assignment, you are to write a simple macro processor, implemented as a programexpand that takes input either fromstdin (if called with no arguments) or from one or more files whose names are provided on the command line (if called with one or more arguments).
Amacro consists of the pipe symbol'|', followed by the name of a readable file, followed by another pipe symbol'|'. Each occurrence of a macro instdin or within a single file should be replaced by the contents of the named file, expanded according to the same rules. The result of these replacements should be sent tostdout. The pipe symbols are referred to as thedelimiters of the macro.
Any character that is not part of a macro is passed tostdout unmodified. Be careful with'\0' characters, particularly ones that appear between delimiters. Null characters can never appear as part of a valid filename, but library functions likefopen have no way to interpret them as anything other than the end of a string.
For example, if filea contains the text
|b|-|b|-|b|fileb contains the text
helloand there is no filec, then running./expand with input
|a|+|b|+|c|should produce output
hello-hello-hello+hello+|c|Note that sincec is not the name of a readable file, the apparent macro|c| is sent tostdout unmodified.
For files that do not contain expandable macros, the behavior ofexpand will be similar to the standard Unix commandcat, which copies tostdout the contents of one or more files named as arguments, or the contents ofstdin if given no arguments.
If a pipe symbol appears in the input text, but is not part of a macro that can be expanded, it should be passed through unmodified and should not affect interpretation of subsequent pipe symbols. For example, in the case above where no filec exists, with input
|c|b|the output should look like
|chelloMacros cannot span multiple files, so a pipe symbol followed by a sequence of non-pipe symbols ending atEOF will never be expanded, even if a subsequent file provides characters that could be used to complete the macro.
If the characters between the delimiters of a macro are a directory name, the behavior of the program is undefined. (This avoids having to work around a bad design decision infopen, which in some circumstances treats directories as empty files.)
Your program is not responsible for detecting or handling infinite recursion, as when a file contains a macro that expands to the file itself. You may also assume that the depth of any recursion is small enough that it is reasonable to use a recursive function for expansion.
Ifexpand is given an argument that is not a readable file, it should print an appropriate error message and return a nonzero exit code. The contents of the error message are up to you (perror may be helpful here), but the nonzero exit code is mandatory in this case. As with macros, the behavior ofexpand if given a directory name as an argument is undefined.
Use the command/c/cs223/bin/submit 6filenames to submit any source files needed to compileexpand. Both the public and final test scripts will attempt to build this program usingmake expand, so if you need to override the default behavior ofmake, you should submit aMakefile as well. As usual, the public test script/c/cs223/Hwk6/test.public can be run directly on the files in your current directory, or on your submitted files using/c/cs223/bin/testit 6 public.
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#define DELIMITER ('|')#define GET_SIZE_INITIAL (256)#define GET_SIZE_MULTIPLIER (2)// Get all characters up to next DELIMITER or EOF.// Returns 1 if found delimiter, 0 if hit EOF.// Null-terminated malloc'd string is assigned to *contents.// Length of string is assigned to *length.intgetDelimited(FILE *f,char **contents,size_t *length){// usual dynamic array thingsize_t size = GET_SIZE_INITIAL;// size of data blocksize_t top =0;// number of characters readchar *data = malloc(size);int eof =1;// got eof? assert(data);int c;// assume eof unless changedwhile((c = getc(f)) != EOF) {if(c == DELIMITER) {// we win eof =0;break; }else {// make room for c and possible extra nullif(top+1 >= size) { size *= GET_SIZE_MULTIPLIER; data = realloc(data, size); assert(data); } data[top++] = c; } }// null doesn't count toward length,// so we won't increment top data[top] = '\0'; *contents = data; *length = top;return eof;}// print DELIMITER followed by stringvoidputDelimited(constchar *contents,size_t length){ putchar(DELIMITER); fwrite(contents, length,sizeof(char), stdout);}// returns 1 if string contains a NUL character// this otherwise allows cheating with fopenintcontainsNull(constchar *contents,size_t length){for(size_t i =0; i < length; i++) {if(contents[i] == '\0') {return1; } }// elsereturn0;}// Expand file given by filename.// For each matching pair of square braces,// if the characters between them names a file that// can be read, replace the braces and the filename// with the expanded contents of the file.voidexpand(FILE *f){int c;while((c = getc(f)) != EOF) {if(c == DELIMITER) {char *contents;size_t length;if(getDelimited(f, &contents, &length)) {// hit EOF, print DELIMITER and contents putDelimited(contents, length); }else {// got second DELIMITER// see if we can open fileFILE *f2;if(!containsNull(contents, length) && (f2 = fopen(contents,"r")) !=0) {// recurse on f2 expand(f2); fclose(f2); }else {// can't open// treat it as text putDelimited(contents, length);// push closing delimiter back to try again ungetc(DELIMITER, f); } } free(contents); }else {// pass it through putchar(c); } }}intmain(int argc,char **argv){if(argc <2) { expand(stdin); }else {for(int i =1; i < argc; i++) {FILE *f;if((f = fopen(argv[i],"r")) ==0) { perror(argv[i]);return1; }else { expand(f); fclose(f); } } }return0;}Pointers can be handy, but C programmers often learn through hard experience that they can be frustrating to deal with if used without discipline. A particularly tempting trap is represent a data type consisting of locations and paths between locations as the obvious data structure consisting of astruct for each location and a pointer for each path. This can quickly turn into a hideous tangle of pointer spaghetti that it can be very difficult to get unstuck from.
For this assignment, you are to rescue an unlucky programmer who has fallen into this trap while implementing a simple text adventure game, by providing an implementation for the functionfreeRooms declared in the header file below:
// A map consists of malloc'd struct rooms, each of which// contains a pointer to a distinct non-null malloc'd string,// and also contains pointers to a room to the north// (or null if there is no such room)// and a room to the south// (or null if there is no such room).struct room {char *description;struct room *north;struct room *south;};// Free every struct room that is either start,// or can be reached from start// by following any sequence of north// and south pointers.// Also free descriptions.// Returns number of rooms freed.size_t freeRooms(struct room *start);This function should free all data, including both thedescription string and thestruct room itself, for eachstruct room that can be reached by starting atstart and followingnorth orsouth pointers. Your function should return the number of rooms freed. You should put your implementation in a single filerooms.c.
You may assume that everystruct room in the data structure has a non-nulldescription field and that the pointers in these fields are all distinct.
Submit your assignment as usual with the command/c/cs223/bin/submit 7 rooms.c. You do not need to submit any other files.
We have provided a test programadventure.c that generates a structure of interconnected rooms from a text file, consisting of one line per room with the formatroom-numbernorth-exitsouth-exitroom-description. The room numbers are translated into pointers upon loading. A small example rooms file isflask.rooms; other rooms files can be found in the/c/cs223/Hwk7 directory in the Zoo.
Compilingadventure.c withrooms.c should produce an executableadventure that takes a rooms file as its first argument and allows the user to navigate between rooms using the commandsn (go north),s (go south), andq (quit). When the user quits,adventure will callfreeRooms starting with the current room, and print the number of reported freed rooms tostderr.
All of the provided rooms files are designed so that if the user quits immediately, every room is reachable from the starting room and should be freed. This can be helpful if usingvalgrind to test if all rooms are indeed freed. If the user moves to a different room, some rooms may no longer be reachable. YourfreeRooms function is not responsible for freeing unreachable rooms and will not be tested forvalgrind errors on any inputs that have unreachable rooms.
A basic test script is available in/c/cs223/Hwk7/test.public. You can run this test script directly on the copy ofrooms.c in your current directory, or run it on your submitted assignment with/c/cs223/bin/testit 7 public.
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include"rooms.h"size_tfreeRooms(struct room *start){// We probably don't have this but let's be safe.if(start ==0) {return0; }// With enough cleverness it is possible to solve this// without using any extra data structures.//// The idea is that we will rearrange the snarl into a single// linked list through the north pointers, and then free all// the nodes in one pass.struct room *last = start;// last processed nodestruct room *middle = start;// first node that might still have a south pointer free(start->description); start->description =0;// Invariants://// Every node initially reachable from start stays reachable// from start.//// Every node from start through last has description == 0//// Every node from start up to but not including middle// has south == 0.while(middle) {if(last->north && last->north->description) {// Follow north pointers until we can't last = last->north; free(last->description); last->description =0; }elseif(middle->south && middle->south->description) {// paste middle->south in place of last->north last->north = middle->south; middle->south =0; middle = middle->north; }else {// clear last->north and advance middle last->north =0; middle = middle->north; } }// now free everythingsize_t count =0;struct room *next;for(struct room *p = start; p; p = next) { next = p->north; free(p); count++; }return count;}For this assignment, you are to write a decoder for a telegraph-stylecommercial code.
Commercial entities that communicated by telegram would often use a codebook that represented common phrases or messages with single words. For example, the codebook might specify thatMAUVE expands toDELIVER FINE QUALITY COPPER INGOTS, saving 20¢ per use in a telegram costing 5¢ per word. Scans of many such codebooks can be found athttps://www.jmcvey.net/cable/scans.htm.
We will not be using a codebook. Instead, each sequence of words contained within a message of the formDEFINEcodeworddefinitionSTOP will define a new mapping fromcodeword todefinition, so that any subsequent appearance ofcodeword will be replaced bydefinition, wheredefinition consists zero or more words. This procedure isnot recursive:definition will be reproduced verbatim without further expansion.
For example, if the encoded message is
DEFINE MAUVE DELIVER FINE QUALITY COPPER INGOTS STOP YOU SAID I WILL MAUVE BUT YOU DID NOT MAUVEthen the decoded message would be
YOU SAID I WILL DELIVER FINE QUALITY COPPER INGOTS BUT YOU DID NOT DELIVER FINE QUALITY COPPER INGOTSIn this particular case, the definition didn’t save us any money, but a longercomplaint letter might allow for additional uses.
You are to write a programexpand that takes an encoded telegram fromstdin, parses any definitions contained in the telegram, and replaces any defined codewords with their definitions, writing the result tostdout.
You may assume that the input consists only of words consisting of upper-case letters and digits, separated by one or more whitespace characters. The behavior or your program for any other input is undefined.
For your output, you should similarly separate words by whitespace. Any choice of whitespace is acceptable, although making the output a single line of words separated by spaces may maximize verisimilitude at the cost of readability.
As described above, definitions are not expanded recursively. The definition of a codeword both at time of definition and at time of expansion is always treated as a sequence of words that are not checked for codewords. Because commercial codes are intended to be usable by humans, you may also assume that definitions are of lengthO(1) as a function of the lengthn of the encoded message. You may assume that all definitions in the input are complete: ifDEFINE appears in a message without a matchingSTOP, the behavior or your program is undefined. You may also assume that the user does not attempt to defineDEFINE orSTOP.
If more than one definition is given for a codeword, the most recent definition applies. For example,DEFINE X Y STOP X DEFINE X Z STOP X expands toY Z.
Use/c/cs223/bin/submit 8files to submit your source files together with aMakefile that will produceexpand using the commandmake expand. (If the defaultmake rules will work for this, theMakefile is optional.)
The command/c/cs223/bin/testit 8 public can be used to run some simple tests on your submitted files. You can also run these tests on the files in your current directory with/c/cs223/Hwk8/test.public.
Seehere. The sample solution uses a mostly standard hash table implementation to store definitions.
Your task for this assignment is to implement a data structure representing an infinite stream of characters. Each such stream is accessed through a pointer to astruct stream, an opaque struct. The next character is read from a stream using thestreamNext function, which has an interface similar togetc for files, except that it never returnsEOF.
There are three constructors for streams:
streamFromString takes a string as its argument, and repeatedly returns the characters in this string, looping back to the start when it runs out. So a stream constructed usingstreamFromString("ab") would alternate between returning'a' and'b'. If called with an empty string as an argument, it returns an infinite sequence of'\0' characters. This function should make a copy of its argument.streamInterleave takes two streams and returns characters from each in alternation. So a stream constructed usingstreamInterleave(streamFromString("a"), streamFromString("b")) should also alternate between returning'a' and'b'.streamMap takes a function pointer and a stream, and transforms the stream by applying the function to each character. You may assume that the function always returns a value in the range 0..255.There is also a single destructorstreamDestroy. This should clean up the stream it is applied to, and also recursively destroy any streams used to construct it. You may assume that no stream is ever used more than once when building a more complex stream.
Definitions for these functions are provided in the filestream.h:
// Infinite streams of characters.//// Stream objects can yield character using streamNext,// or can be used to construct other streams.//// When constructing a composite stream, each stream// should be used only once. This avoids oddities where// characters from some stream are consumed in multiple// places, and allows all streams used in the construction// to be freed using a single call to streamDestroy.typedefstruct stream Stream;// Return the next character in streamint streamNext(Stream *stream);// Return a stream that repeats the contents of s// over and over.// For empty string, returns infinite stream of '\0'// This should copy s in case s is changed or freed.//// streamFromString("abc") -> "abcabcabcabcabcabc..."Stream *streamFromString(constchar *s);// Return contents of even interleaved with contents of odd.//// streamInterleave(streamFromString("a"), streamFromString("bc"))// -> "abacabacabac..."Stream *streamInterleave(Stream *even, Stream *odd);// Return stream where each character c is replaced// by f(c). Both c and f(c) should be in the range 0..255.Stream *streamMap(int (*f)(int), Stream *stream);// Free stream and any streams used to construct stream.void streamDestroy(Stream *stream);A basic test program is provided inmain.c. This parses a description in prefix notation fromstdin into a tree of calls to the various constructor functions, then appliesstreamNext to obtain characters from the resulting stream that are either printed directly tostdout or used as input to a hash function whose value is printed tostdout.
Submit your assignment as usual using the command/c/cs223/bin/submit 9 stream.c. You do not need to submit any other files. You can run the public test script in/c/cs223/Hwk9/test.public on your submitted assignment with the command/c/cs223/bin/testit 9 public to verify that the submission worked.
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include<string.h>#include"stream.h"#define INTERLEAVED_STREAMS (2)// We have few enough cases that we will just// glob everything together.// Unused pointer fields will be set to null.struct stream {// Advances the stream and returns first characterint (*next)(Stream *);// malloc'd string or null// used for streamFromStringchar *s;// pointer into schar *sNext;// used for streamMapint (*map)(int);// Streams to merge and mod 2 counter.// For streamMap, only streams[0] is used. Stream *streams[INTERLEAVED_STREAMS];int rotor;};// create a stream with default valuesstatic Stream *streamCreate(void){ Stream *s = malloc(sizeof(struct stream)); s->next =0; s->s =0; s->sNext =0; s->map =0;for(int i =0; i < INTERLEAVED_STREAMS; i++) { s->streams[i] =0; } s->rotor =0;return s;}staticintnextFromString(Stream *s){if(*s->s == '\0') {return '\0'; }else {if(*s->sNext == '\0') {// rewind s->sNext = s->s; }return *s->sNext++; }}Stream *streamFromString(constchar *s){ Stream *str = streamCreate(); str->next = nextFromString; str->s = str->sNext = strdup(s);return str;}intnextInterleave(Stream *s){ Stream *next = s->streams[s->rotor]; s->rotor = !s->rotor;return streamNext(next);}Stream *streamInterleave(Stream *even, Stream *odd){ Stream *s = streamCreate(); s->next = nextInterleave; s->streams[0] = even; s->streams[1] = odd;return s;}intnextMap(Stream *s){return s->map(streamNext(s->streams[0]));}Stream *streamMap(int (*f)(int), Stream *ss){ Stream *s = streamCreate(); s->next = nextMap; s->map = f; s->streams[0] = ss;return s;}intstreamNext(Stream *s){return s->next(s);}voidstreamDestroy(Stream *s){// free, close, or destroy anything that isn't nullif(s->s) { free(s->s); }for(int i =0; i < INTERLEAVED_STREAMS; i++) {if(s->streams[i]) { streamDestroy(s->streams[i]); } } free(s);}The release of Trash Collector Tenth Anniversary Edition™ has created a demand for automated tools for players who want to revisit the exciting gameplay of their youth without having to deal with the boring parts. The most in-demand tool is a mechanism for inventory management, automatically discarding the least valuable game items in the player’s possession whenever they exceed their total weight limit.
Your task is to write a programinventoryManager that performs this service. The input to your program, presented onstdin, will consist of a single line specifying the weight limit, followed by a sequence of lines, one per item, that specifies the price of the item, its weight, and its name, separated by spaces. Your program should track the set of items acquired at any point, and if the total weight exceeds the weight limit, it should discard the items with the worst price-to-weight ratios until the weights of the remaining items sum to no more than the weight limit. Discarding an item is done by printing out its price, weight, and name tostdout. Your program should exit upon receivingEOF.
You may assume that the total weight limit and the weight and price of each item is a positive integer value that fits in anint. You may assume that the name of each item starts with a non-space character and can be stored as a null-terminated string. This means that the initial input line can be parsed withscanf("%d", ...) and subsequent input lines can be parsed withscanf("%d %d %m[^\n]", ...), but you can use your own input routines if you prefer.
Suppose your program is given the input:
101 1 Cabbage4 1 Skooma30 5 Eidar Cheese Wheel1 2 Soylent McDovahkiin Burger 10 4 Geralt's Wisdom Teeth5 1 Amulet of Yendor100 8 Orcish Greatsword4 3 Potion of Inadequate Healing1 1 AppleThe first line assigns a weight limit of 10. The first four items sum to only 9, but addingGeralt's Wisdom Teeth brings the total weight to 13, which is over the limit. The most worthless item measured by price/weight is theSoylent McDovahkiin Burger at 1/2, so we discard this to bring the weight back down to 11. Discarding theCabbage then brings us down to 10. Picking up theAmulet of Yendor forces us to drop the next worst item,Geralt's Wisdom Teeth. Next, theOrcish Greatsword fills most of our inventory while having a better price/weight ratio than any of our other possessions, forcing us to flush everything else in order of increasing price/weight ratio. ThePotion of Inadequate Healing has a low price/weight ratio and doesn’t fit, so it is discarded immediately. We are left at the end with just theOrcish Greatsword and anApple, for a total price of 101 and a total weight of 9. The output will look like this:
1 2 Soylent McDovahkiin Burger 1 1 Cabbage10 4 Geralt's Wisdom Teeth4 1 Skooma5 1 Amulet of Yendor30 5 Eidar Cheese Wheel4 3 Potion of Inadequate HealingNote that in this particular example, our greedy strategy of dropping items with the worst price/weight ratio first is not optimal, because we could have discarded theEidar Cheese Wheel earlier and saved theSkooma andAmulet of Yendor to get a total price of 109. But for this assignment, you should abide strictly by the suboptimal greedy strategy despite any temptations to improve on it.
While it did not occur in this example, it may be the case that you are given the option to discard two items with the same price/weight ratio. You may break such ties arbitrarily.
Submit whatever files are needed to build your program using/c/cs223/bin/submit 10files. The test script will runmake inventoryManager to generate your program. You can use the command/c/cs223/bin/testit 10 public to run the public test script in/c/cs223/Hwk10/test.public on your submitted files.
We start by implementing a heap with rational-valued keys:
// Implements a min-heap with rational keys// expressed as numerator/denominator,// both of type uint32_t.//// Each element also has a string label.// This label is *not* copied on insert;// instead, the RatHeap takes control and// eventually either frees the label itself// or returns it for the caller to free.struct elt {uint32_t numerator;uint32_t denominator;char *label;// malloc'd data};typedefstruct ratHeap RatHeap;RatHeap *ratHeapCreate(void);void ratHeapDestroy(RatHeap *);// takes control of e.labelvoid ratHeapInsert(RatHeap *,struct elt e);int rationHeapIsEmpty(const RatHeap *);// caller should free labelstruct elt ratHeapDeleteMin(RatHeap *);#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include"ratHeap.h"struct ratHeap {size_t n;// number of elts in usesize_t size;// number of slots in datastruct elt *data;// malloc'd data block};#define INITIAL_SIZE (16)#define SIZE_MULTIPLIER (2)RatHeap *ratHeapCreate(void){ RatHeap *h = malloc(sizeof(*h)); assert(h); h->n =0; h->size = INITIAL_SIZE; h->data = calloc(h->size,sizeof(struct elt)); assert(h->data);return h;}voidratHeapDestroy(RatHeap *h){for(size_t i =0; i < h->n; i++) { free(h->data[i].label); } free(h->data); free(h);}staticsize_tparent(size_t i){return (i-1)/2;}staticsize_tchild(size_t i,int kid){return2*i +1 + kid;}staticvoidswap(struct elt *x,struct elt *y){struct elt tmp = *x; *x = *y; *y = tmp;}// returns 1 if x > y when comparing numerator/denominatorstaticintisSmaller(struct elt x,struct elt y){// Compute common numerators.// Using uint64_t avoids overflowuint64_t xx = ((uint64_t) x.numerator) * ((uint64_t) y.denominator);uint64_t yy = ((uint64_t) y.numerator) * ((uint64_t) x.denominator);return xx < yy;}// float i down until heap property restoredstaticvoidfloatDown(RatHeap *h,size_t i){while(child(i,0) < h->n) {// check s[i] against its childrensize_t smaller = child(i,0);// check for right child being even smallerif(child(i,1) < h->n && isSmaller(h->data[child(i,1)], h->data[child(i,0)])) { smaller = child(i,1); }// am I smaller than my smaller child?if(isSmaller(h->data[smaller], h->data[i])) {// swap swap(&h->data[i], &h->data[smaller]); i = smaller; }else {// donereturn; } }}// float i up until heap property restoredstaticvoidfloatUp(RatHeap *h,size_t i){while(i >0 && isSmaller(h->data[i], h->data[parent(i)])) { swap(&h->data[i], &h->data[parent(i)]); i = parent(i); }}voidratHeapInsert(RatHeap *h,struct elt e){// avoid any hint of dividing by 0 assert(e.denominator !=0);if(h->n >= h->size) { h->size *= SIZE_MULTIPLIER; h->data = realloc(h->data, h->size *sizeof(struct elt)); }// insert at end and float p h->data[h->n] = e; floatUp(h, h->n); h->n++;}intratHeapIsEmpty(const RatHeap *h){return h->n ==0;}struct eltratHeapDeleteMin(RatHeap *h){ assert(!ratHeapIsEmpty(h));struct elt ret = h->data[0]; h->data[0] = h->data[--(h->n)]; floatDown(h,0);return ret;}The main program is relatively short:
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include"ratHeap.h"intmain(int argc,char **argv){if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; }uint32_t limit;// spec says %d, but also guarantees positive,// so this should workif(scanf("%u", &limit) !=1) { fprintf(stderr,"Bad weight limit\n");return1; } RatHeap *h = ratHeapCreate();uint32_t total =0;// total denominator of everything in hstruct elt e;while(scanf("%u %u %m[^\n]", &e.numerator, &e.denominator, &e.label) ==3) { ratHeapInsert(h, e); total += e.denominator;while(total > limit) {// remove worst element// since total > 0 this should be safestruct elt worst = ratHeapDeleteMin(h); total -= worst.denominator; printf("%u %u %s\n", worst.numerator, worst.denominator, worst.label); free(worst.label); } } ratHeapDestroy(h);return0;}These files can be compiled with a simpleMakefile.
For this assignment, you are to implement an abstract data typeVector that acts like an array that allowsint values to be read from or written to specific indexes, and also allows a new value to be inserted at a given index.
Declarations for the functions for this abstract data type are given in the filevector.h:
// Vector data type.//// Acts like an array of ints,// but allows insertion of new slots.typedefstruct vector Vector;// Create a new vector with indices 0..n-1// and all values initialized to value.Vector *vectorCreate(size_t n,int value);// Destroy a vector, freeing all space.void vectorDestroy(Vector *);// Return the number of slots in a vectorsize_t vectorSize(const Vector *);// Return a pointer to position i in a vector.// This is equivalent to a+i for an array.//// The pointer is only guaranteed to be valid// until the next insert or delete operation.//// Return 0 if i is out of range.int *vectorRef(Vector *,size_t i);// Insert a new slot at position i.// Slots previously at positions i or// higher will move up by 1.//// New slot is initialized to value.void vectorInsert(Vector *,size_t i,int value);A vector is created withvectorCreate, which takes a length and common initial value as arguments. For example, when called asvectorCreate(5, 2), the resulting vector will contains the values2 2 2 2 2. ThevectorDestroy function frees any allocated memory used by a vector.
ThevectorRef function can be used to get a pointer to a particular location in the vector, which remains valid as long until the next call tovectorInsert orvectorDestroy on this vector. So the lines
Vector *v = vectorCreate(5, 2); *vectorRef(v, 3) = 4;will produce a vector holding values2 2 2 4 2.
IfvectorRef is given an out-of-bounds index, it should return a null pointer. ThevectorSize function can be used to determine the size of a vector, which will be one greater than the maximum index.
ThevectorInsert function grows the vector by inserting a new element at a given position. The previous element at this position (if any) and any subsequent elements are moved up one position, and the size of the vector increases by one.
If the new position is equal to the current size of the vector,vectorInsert should append to the end of the vector. If it is larger,vectorInsert should have no effect.
So after running
Vector *v = vectorCreate(5, 2); vectorInsert(v, 0, 4); vectorInsert(v, 6, 7); vectorInsert(v, 2, 5);the vectorv should contain the values4 2 5 2 2 2 2 7.
Your task is to provide an implementation of these functions in a source filevector.c.
While it is possible to implement a vector as a dynamic array, this may yield bad performance forvectorInsert. You should aim for an implementation in whichvectorRef andvectorInsert both takeO(log n) time.
Submit your assignment as usual with/c/cs223/bin/submit 11 vector.c.
We have provided a test harnessvectorTest.c that can be compiled together withvector.c to produce a programvectorTest that maintains a vector and allows operations to be applied to it using some rudimentary commands. This program is used by the public test script/c/cs223/Hwk11/test.public. You can run this test script on your submitted file with/c/cs223/bin/testit 11 public.
Because some of the test cases for this assignment are fairly large, you may find the public test script runs for longer than you are used to. This should not be a problem for the grading as long as you are staying within the timeouts, but for testing you might may find it convenient to make your own smaller tests.
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include"vector.h"// Implementation strategy: augmented balanced BSTs.//// We will use randomized treaps for balancing.// Some of this is adapted from lecture examples.#define NUM_KIDS (2)#define LEFT (0)#define RIGHT (1)#define EMPTY (0)// null pointer// note: we don't have keys, we just have orderstruct node {int value;int size;// size of subtreeint priority;// for max heapstruct node *kids[NUM_KIDS];};struct vector {struct node *root;};Vector *vectorCreate(size_t n,int value){// we'll start with an empty vector and insert n slots Vector *v = malloc(sizeof(*v)); assert(v); v->root = EMPTY;for(size_t i =0; i < n; i++) { vectorInsert(v,0, value); }return v;}staticvoidvectorDestroyNodes(struct node *root){if(root != EMPTY) {for(int i = LEFT; i <= RIGHT; i++) { vectorDestroyNodes(root->kids[i]); } free(root); }}voidvectorDestroy(Vector *v){ vectorDestroyNodes(v->root); free(v);}staticsize_tnodeSize(struct node *root){if(root == EMPTY) {return0; }else {return root->size; }}size_tvectorSize(const Vector *v){return nodeSize(v->root);}int *vectorRef(Vector *v,size_t i){// use iterative version for speedstruct node *root = v->root;while(root != EMPTY) {struct node *left = root->kids[LEFT];int leftSize = nodeSize(left);if(i < leftSize) { root = left; }elseif(i == leftSize) {return &root->value; }else { root = root->kids[RIGHT]; i = i - leftSize -1; } }// out of rangereturn0;}staticvoidnodeFixSize(struct node *root){ root->size =1;for(int i = LEFT; i <= RIGHT; i++) { root->size += nodeSize(root->kids[i]); }}// rotate (*root)->kid[dir] in place of *rootstaticvoidnodeRotateUp(struct node **root,int dir){struct node *x = *root;struct node *y = x->kids[dir]; *root = y; x->kids[dir] = y->kids[!dir]; y->kids[!dir] = x;// size of x and y might change// must fix x first since it is now child nodeFixSize(x); nodeFixSize(y);}// insert node after i existing nodesstaticvoidnodeInsert(struct node **root,size_t i,struct node *new){if(*root == EMPTY) {// OK to insert at 0 in empty treeif(i ==0) { *root = new; nodeFixSize(new); }else { free(new); } }else {size_t leftSize = nodeSize((*root)->kids[LEFT]);int side;// which side to insert intoif(i <= leftSize) { side = LEFT; }else { side = RIGHT; i = i - leftSize -1; }// insert in subtree// note: this might fail if i is too big nodeInsert(&(*root)->kids[side], i, new);// maybe float up new subtree rootif((*root)->kids[side] != EMPTY) {if((*root)->kids[side]->priority > (*root)->priority) {// this also fixes size nodeRotateUp(root, side); }else {// fix size anyway nodeFixSize(*root); } } }}voidvectorInsert(Vector *v,size_t i,int value){struct node *new = malloc(sizeof(*new)); assert(new); new->value = value; new->size =1; new->priority = rand();for(int i = LEFT; i <= RIGHT; i++) { new->kids[i] = EMPTY; } nodeInsert(&v->root, i, new);}Themajority dynamics process involves a graph where each node is labeled with a color, and at each step of the process, the node updates its label to be the majority label among its neighbors, retaining its previous color in the case of a tie.
Since we are using ASCII input and output, we will represent the two colors as'.' and'X'. For example, the rule tells us that if a node with two neighbors has label'.' at time 0, then it will switch to'X' at time 1 if, and only if, both of its neighbors have label'X' at time 0.
The state of the graph will be represented in both the input and the output as a grid of fixed size. Some positions in the grid may be used to hold characters that are purely decorative and are not updated by the majority dynamics rule.
Your task is to write a programmajority that takes a count of steps, a description of the initial graph state, and a list of edges for the graph, and runs majority dynamics on this graph for the specified number of steps. Details of the precise input and output formats are given below.
The input to your program will consist of
'.' and'X' represent the two colors. Any other characters are fixed. Fixed characters are not updated by the majority dynamics rule and are printed as-is.Note that some edges may have endpoints that are labeled with characters other than'.' or'X'. These endpoints should not be counted or updated when applying the majority dynamics rule.
The output of your program should reproduce the initial grid of input characters, and should produces additional grids depicting the result of applying each step of the majority dynamics process. These grids should all consist ofr lines ofc characters each, and should be separated by newlines, leaving blank lines in between the grids.
The input
32 2X..X0 0 0 10 0 1 00 0 1 10 1 1 00 1 1 11 0 1 1specifies a graph with four nodes in a 2-by-2 grid, with all nodes connected to each other. The number at the top says to run the process on this graph for 3 steps. The corresponding output will be
X..X.XX.X..X.XX.This displays the initial state and the states after 1, 2, and 3 steps. In each step, each node switches color according to the majority dynamics rule, because two of its three neighbors have the opposite color.
A more complex example is this graph:
55 5.-X-.|\|/|.-X-.|/ \|.-X-X0 0 0 20 0 2 00 0 2 20 2 0 40 2 2 20 4 2 20 4 2 42 0 2 22 0 4 02 2 2 42 2 4 02 2 4 42 4 4 44 0 4 24 2 4 4Here various ASCII art characters are used to draw edges between nodes. These edges in the grid are for illustration only, but they happen to correspond to the edges given numerically later. Running./majority on this input produces:
.-X-.|\|/|.-X-.|/ \|.-X-XX-.-X|\|/|.-.-X|/ \|X-X-X.-X-.|\|/|X-X-X|/ \|.-X-XX-.-X|\|/|.-X-X|/ \|X-X-X.-X-X|\|/|X-X-X|/ \|X-X-XX-X-X|\|/|X-X-X|/ \|X-X-XUse the command/c/cs223/bin/submit 12files to submit whatever C source and header files you need, and an optionalMakefile if desired. The test script will use these files to build your program with the commandmake majority.
A public test script is available on the Zoo in/c/cs223/Hwk12/test.public. This can be run on your submitted files with the command/c/cs223/bin/testit 12 public. The sample inputs and outputs used by the script are found in the directory/c/cs223/Hwk12/testFiles.
// Majority dynamics on a graph packed into a picture#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include<stddef.h>#define COLOR0 ('.')#define COLOR1 ('X')#define NUM_COLORS (2)staticchar Colors[NUM_COLORS] = { COLOR0, COLOR1 };intcolorToIndex(char color){return color != COLOR0;}charindexToColor(int index){return Colors[index];}intisActiveColor(char color){return color == COLOR0 || color == COLOR1;}// we will store cell state separately from edgesstruct cell {char color;// a characterptrdiff_t count;// COLOR1 neighbors minus COLOR0 neighbors};struct cellList {size_t rows;size_t columns;size_t n;// rows * columns, for conveniencestruct cell c[];// packed data};// edges will just be in a big arraystruct edge {// these are indices into cellListsize_t s;size_t t;};struct edgeList {size_t n;// number of actual edgessize_t capacity;// number of allocated edgesstruct edge e[];};size_tindexPack(conststruct cellList *c,size_t row,size_t column){return row * c->columns + column;}struct cellList *cellListParse(void){size_t rows;size_t columns;if(scanf("%zu %zu", &rows, &columns) !=2) { fprintf(stderr,"Couldn't parse row and column counts.\n"); exit(1); }// eat the newlineint c = getchar(); assert(c =='\n');struct cellList *cells = malloc(sizeof(struct cellList) + rows * columns *sizeof(struct cell)); assert(cells); cells->rows = rows; cells->columns = columns; cells->n = rows * columns;size_t pos =0;for(size_t i =0; i < rows; i++) {for(size_t j =0; j < columns; j++) { cells->c[pos++].color = getchar(); }// eat the newlineint c = getchar(); assert(c =='\n'); }return cells;}voidcellListDisplay(conststruct cellList *cells){size_t i =0;for(size_t row =0; row < cells->rows; row++) {for(size_t column =0; column < cells->columns; column++) { putchar(cells->c[i++].color); } putchar('\n'); }}#define EDGES_INITIAL (16)#define EDGES_MULTIPLIER (2)struct edgeList *edgeListParse(conststruct cellList *cells){struct edgeList *edges = malloc(sizeof(struct edgeList) +sizeof(struct edge) * EDGES_INITIAL); assert(edges); edges->n =0; edges->capacity = EDGES_INITIAL;size_t r1;size_t c1;size_t r2;size_t c2;while(scanf("%zu %zu %zu %zu", &r1, &c1, &r2, &c2) ==4) {if(edges->n >= edges->capacity) { edges->capacity *= EDGES_MULTIPLIER; edges = realloc(edges,sizeof(struct edgeList) +sizeof(struct edge) * edges->capacity); assert(edges); } edges->e[edges->n].s = indexPack(cells, r1, c1); edges->e[edges->n].t = indexPack(cells, r2, c2); edges->n++; }return edges;}struct state {struct cellList *cells;struct edgeList *edges;};intcolorToDelta(char color){switch(color) {case COLOR0:return -1;case COLOR1:return1;default:return0; }}voidstateUpdate(struct state *s){// get countsfor(size_t i =0; i < s->cells->n; i++) { s->cells->c[i].count =0; }for(size_t i =0; i < s->edges->n; i++) {struct edge e = s->edges->e[i]; s->cells->c[e.s].count += colorToDelta(s->cells->c[e.t].color); s->cells->c[e.t].count += colorToDelta(s->cells->c[e.s].color); }// update colorsfor(size_t i =0; i < s->cells->n; i++) {if(isActiveColor(s->cells->c[i].color)) {if(s->cells->c[i].count >0) { s->cells->c[i].color = COLOR1; }elseif(s->cells->c[i].count <0) { s->cells->c[i].color = COLOR0; } } }}intmain(int argc,char **argv){if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; }size_t steps;if(scanf("%zu", &steps) !=1) { fprintf(stderr,"Couldn't parse total steps\n"); exit(1); }struct state s; s.cells = cellListParse(); s.edges = edgeListParse(s.cells); cellListDisplay(s.cells);for(size_t step =0; step < steps; step++) { stateUpdate(&s); putchar('\n'); cellListDisplay(s.cells); } free(s.cells); free(s.edges);return0;}This should build with the defaultmake rules, but just in case, here is aMakefile.
Assignments are generally due Thursdays at 17:00 Eastern US time, except when this would fall on a break day or immediately after a break day.
Make sure that you have an account on the Zoo. This should have been created automatically when you registered for the class, but if not, you will need to contact ITS atcs.support@yale.edu. You will also need a course directory in/c/cs223/class. This should also have been created automatically, but if you have a Zoo account but do not have a course directory, you can create one using the commandsudo register cs223 in the Zoo.
You do not need to develop your solution on the Zoo, but you will need to turn it in there, and it will be tested using the compiler on the Zoo. So it’s best to make sure you have access to the Zoo as soon as possible.
For this assignment, you are to write two programs for decoding and encoding a simple data compression scheme known asrun-length encoding. The idea of run-length encoding is to representruns of multiple copies of the same character as a count plus a single copy of the character. For inputs with many long runs, this can lead to significant reduction in length.
We will be asking you to implement a particular run-length encoding scheme designed to handle arbitrary inputs. A run is encoded by a digit ('0'..'9') followed by the character. The digit indicates the number ofextra copies of the following character to include, so, for example, the encoded string2z represents the unencoded stringzzz. A run is encoded only if it would reduce the total length or if the run consists of one or more copies of a digit. If a run consists of more than 10 copies of a character, it is encoded as a sequence of runs of length 10 followed by whatever characters are left over.
Some examples of strings and their encodings:
| String | Encoding |
moo | moo |
mooo | m2o |
mooooooooooo | m9oo |
1 | 01 |
11 | 11 |
(The1 example shows that run-length encoding does not always reduce the size of its input. This is an unavoidable property of all lossless compression schemes. For a well-designed scheme, the hope is that the inputs that get shorter are more likely than the ones that get longer.)
There is no special handling for characters other than digits. For example, this classic bit of20th-century ASCII art:
.-_|\ / \ Perth ->*.--._/ v <- Tasmaniacompresses to:
9 4 .-_|\9 3 /4 \5 Perth ->*.--._/9 8 v <- TasmaniaDecoding this scheme is straightforward: on encountering a digitd (which you can test using theisdigit macro, defined in header filectype.h), outputd+1 copies of the following character (unless it isEOF, in which case you should just exit). Encoding may require more cleverness.
Your task is to write programsdecode.c andencode.c that decode and encode this particular run-length encoding scheme. Each should take its input fromstdin and write its output tostdout, continuing until it hitsEOF. We recommend that you writedecode.c first, since it’s easier.
Some sample inputs and outputs can be found in the directory/c/cs223/Hwk1/testFiles in the Zoo, though you should also feel free to create test inputs of your own. Note that some of the sample files contain non-printing characters that may have odd effects if you send them to your screen. The safest way to test if your program produces the same output as the sample output is probably to usecmp, for example:
$ ./encode < test.in > tmp$ cmp tmp test.outIftmp andtest.out contain the same characters,cmp will say nothing. Otherwise it will tell you the first position where they differ.
If you do want to see the characters in a binary file, trying usingod, as in
$ echo hi > hi.txt$ od -t x1 -t c hi.txt0000000 68 69 0a h i \n0000003Submit your assignment using the command:
/c/cs223/bin/submit 1 decode.c encode.cYou can test that your submitted programs compile (and pass a few basic tests) using the command:
/c/cs223/bin/testit 1 publicThis runs the test script in /c/cs223/Hwk1/test.public on the submission directory. You can also run the same script by hand on your unsubmitted code using
/c/cs223/Hwk1/test.publicThe inputs and outputs this script expects can be found in the directory /c/cs223/Hwk1/testFiles/, in case you want to look more closely at what each test is doing.
The unsympathetic robo-grading script used to grade this assignment may or may not use the same tests as this command, so you should make sure your program works on other inputs as well. You may also want to look at thestyle grading checklist to see that you haven’t committed any gross atrocities against readability, in case a human being should happen to look at your code.
You can submit your assignment more than once, but any late penalties will be assessed based on the last submission. The command
/c/cs223/bin/protect 1 decode.c encode.cwill protect your assignment against future modification. This is highly recommended to avoid accidentally overwriting your submission and incurring unwanted late penalties. Replaceprotect withunprotect if you need to re-enable changes.
For more details about thesubmit script and its capabilities, seehere.
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include<ctype.h>intmain(int argc,char **argv){if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; }// Simple RLE decoder.//// A digit x in 0-9 indicates the following character// (if not EOF)// should be repeated an extra x times.// Example: 01234a -> 1333aaaaaint c;// current characterint cnext;// next characterwhile((c = getchar()) != EOF) {if(isdigit(c)) {// grab another character and repeat itif((cnext = getchar()) != EOF) {for(int count =0; count < c -'0' +1; count++) { putchar(cnext); } } }else {// no repeat, just copy through putchar(c); } }return0;}#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include<ctype.h>#define MAX_RUN (10)// longest run to encodevoidprintWithCount(int count,int c){ putchar(count -1 +'0'); putchar(c);}intmain(int argc,char **argv){if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; }// Simple RLE encoder.//// A digit x in 0-9 indicates the following character// should be repeated an extra x times.//// To allow digits in input, digits are always encoded.//// Non-digits are only encoded if doing so reduces length,// which means when there are three or more copies.//// Example: 1aabbb -> 01aa2b//// If a character occurs more than 10 times in a row, encode// it in blocks of 10 as long as possible://// Example: xxxxxxxxxxxxxxxxx -> 9x6xint c;// character to encodeint cnext;// next characterint count;// how many c's did we get?while((c = getchar()) != EOF) {// count the c's up to MAX_RUNfor(count =1; (cnext = getchar()) == c && count < MAX_RUN; count++);// push back extra character ungetc(cnext, stdin);if(isdigit(c)) {// always use count printWithCount(count, c); }else {switch(count) {case2: putchar(c);// fall throughcase1: putchar(c);break;default: printWithCount(count, c);break; } } }return0;}For this assignment, you are to write a functionprintFixed that prints fixed-point decimal values tostdout with thousands grouping using given separator and decimal point characters, and with a given precision specifying the number of digits to print after the decimal point. The interface toprintFixed is given in the file/c/cs223/Hwk2/printFixed.h, of which the important part is the declaration
You are to supply an implementation of this function in a fileprintFixed.c. You should include the line
near the top of your source file to ensure that the compiler can check that your implementation matches the specification. (You may need to copyprintFixed.h into your compilation directory to make this work.)
Thenumber argument toprintFixed specifies the integer value that you would get if you multiplied the desired output value by 10precision. For example, ifprecision is 2, andnumber is 12345, thennumber should be interpreted as the value 123.45.
If the value is negative, the output should start with a minus sign'-', and otherwise should be formatted like a non-negative value.
If the value has absolute value at least 1000, you will need to group digits in groups of three, using the characterseparator to separate them. (This is the usual convention for languages other than Hindi, which may be a little rude considering who invented this notation, but it’s what we’ve got.) ThedecimalPoint parameter specifies what character to put before the fractional part of the number. The fractional part should always be printed with exactlyprecision digits, even if this would leave trailing zeros.
If the integer part of the number is 0, a leading zero should be printed. Otherwise the output should start with the first nonzero digit of the integer part.
Some examples of numbers formatted withseparator',' anddecimalPoint'.':
number | precision | Output |
|---|---|---|
| 123456789 | 0 | 123,456,789. |
| 123456789 | 2 | 1,234,567.89 |
| -123456789 | 2 | -1,234,567.89 |
| 123456789 | 12 | 0.000123456789 |
| -123456789 | 12 | -0.000123456789 |
Your implementation should be able to handle any number that can fit inlong long, which means any number in the rangeLLONG_MIN throughLLONG_MAX (these are defined in/usr/include/limits.h). You may assume thatprecision is small enough that 10precision fits in along long, and if this condition is violated, your function can do whatever it wants.
A simple wrapper forprintFixed can be found in/c/cs223/Hwk2/main.c. This takes four arguments specifyingnumber,separator,decimalPoint, andprecision, and callsprintFixed with the parsed values of these arguments. You will need to compile this together withprintFixed.c to get a working program. For example:
$ gcc -g3 -Wall -o main main.c printFixed.c$ ./main -123456789 , . 2-1,234,567.89You should also feel free to write your own testing code if this would be helpful.
Submit your assignment as usual using the command:
/c/cs223/bin/submit 2 printFixed.cYou do not need to submit any other files. In particular,printFixed.h will be supplied by the test script, so you should not try to modify or replace this file.
You can test that your submission complies (and passes a few basic tests) using the command:
/c/cs223/bin/testit 2 publicThis runs the test script in /c/cs223/Hwk1/test.public on the submission directory. You can also run the same script by hand on your unsubmitted code using
/c/cs223/Hwk2/test.public#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include"printFixed.h"// compute base**exponent// doesn't deal with overflowstaticlonglongintExp(longlong base,unsignedint exponent){longlong result =1;while(exponent-- >0) { result *= base; }return result;}// print a single digitstaticvoidprintDigit(int digit){ putchar('0' + digit);}// probably don't need this level of generality,// but a good habit#define BASE (10)// base for decimal expansion#define DIGITS_PER_GROUP (3)// digits per groupvoidprintFixed(longlong number,char separator,char decimalPoint,unsignedint precision){unsignedlonglong abs;// absolute value of number// print sign bit if needed and convert to unsigned long longif(number <0) { putchar('-'); abs = -number; }else { abs = number; }// split up abs into integer and fractional partsunsignedlonglong divisor = intExp(BASE, precision);unsignedlonglong integerPart = abs / divisor;unsignedlonglong fractionalPart = abs % divisor;// for grabbing digitsint digit =0;// which digit should we print next?unsignedlonglong digitValue =1;// value of digit position, 10^digitint firstDigit;// first digit// print the integerPart// 0 is a special caseif(integerPart ==0) { printDigit(0); }else {// find first digit// we have to be a little careful here to prevent overflowwhile(integerPart / digitValue >= BASE) { digit++; digitValue *= BASE; }while(digit >=0) {// print first digit firstDigit = integerPart / digitValue; printDigit(firstDigit);// remove first digit integerPart -= firstDigit * digitValue;// maybe print separatorif(digit % DIGITS_PER_GROUP ==0 && digit !=0) { putchar(separator); }// update digit and digitValue digit--; digitValue /= BASE; } }// print decimal point putchar(decimalPoint);// print remaining digits// we can't just use printf for this because we can't// specify width. So we'll re-use digit and digitValue. digit = precision -1; digitValue = divisor /10;while(digit >=0) {// there is some duplication here// but it is annoying to factor out firstDigit = fractionalPart / digitValue; printDigit(firstDigit); fractionalPart -= firstDigit * digitValue; digit--; digitValue /= BASE; }}For this assignment, you are to simulate an automated self-programming pancake-flippng machine. This machine functions as a stored-program computer where the memory of the computer is a stack of pancakes, each with a numerical value that is anunsigned int, and the main computational operation is to flip some consecutive subsequence of the pancakes. Additional operations are provided to print values from memory and manage control flow.
The interface to this machine is a functionrunPancakeProgram declared in the filepancake.h as follows:
// run the program contained in memoryvoid runPancakeProgram(unsignedint *memory);// Convenience names for pancake-flipper instructions:#define FLIP (0)#define PRINT (1)#define JUMP (2)#define SKIP (3)#define STOP (4)This file also contains#defines for the instruction set of the machine (see below). These#defines are provided for programmer convenience, but the numerical values of the instructions are fixed and guaranteed not to change.
The pancake machine has five instructions, each of which takes zero or more arguments. The behavior of each instruction is given by the following table:
| Name | Encoding | Arguments | Effect |
|---|---|---|---|
FLIP | 0 | xy | Flipmemory[x] throughmemory[y-1] |
PRINT | 1 | xy | Printmemory[x] throughmemory[y-1] |
JUMP | 2 | x | Set program counter tox |
SKIP | 3 | xy | Skip next instruction ifmemory[x] > memory[y] |
STOP | other | none | Stop executing program and return |
TheFLIP operation reverses the order of the contents ofmemory[x] throughmemory[y-1]. For example, if thememory array hasn elements, thenFLIP 0 n will reverse the entire array.
ThePRINT operation prints the numerical values ofmemory[x] throughmemory[y-1] tostdout, separated by spaces, and with a newline following the last value. Each value should be formatted in the same format as produced byprintf("%u"). There should be no space following the last value.
TheJUMP instruction causes execution to switch to the specified position in the program.
TheSKIP instruction comparesmemory[x] tomemory[y]. If the former is strictly greater than the latter, the next instruction is not executed and control proceeds to the following instruction instead.
Any other numerical value is interpreted asSTOP. TherunPancakeProgram function should return immediately upon reaching aSTOP instruction.
Instructions are normally executed in sequence, althoughJUMP andSKIP operations can change this. For example, after executing aFLIP orPRINT instruction, the next instruction (starting after the arguments to theFLIP orPRINT) is executed.
The behavior ofrunPancakeProgram is undefined if:
The program stored inmemory executes aFLIP x y orPRINT x y instruction wherex is strictly greater thany ory is strictly greater than the number of values inmemory.
ASKIP operation attempts to access a value outside the range ofmemory.
The program counter advances past the last position inmemory, through normal control flow, aJUMP instruction, or aSKIP instruction.
You are not required to handle any of these cases, and you may assume that you will not be given a program that causes any of them to occur ifrunPancakeProgram is otherwise correct.
Suppose the contents ofmemory are2 4 0 1 1 2 4 3 3 2 4 0 2 4 2 4.
We can parse this as a sequence of instructions and data as follows, where the labels before the colons are indices:
0: 2 4 # JUMP to 4 2: 0 1 # data 4: 1 2 4 # PRINT locations 2 and 3 (the data) 7: 3 3 2 # SKIP if location 3 > location 210: 4 # STOP11: 0 2 4 # FLIP locations 2 and 314: 2 4 # JUMP to 4When we execute this program, the program counter starts at 0, but then jumps to 4 (skipping over the two data values). At position 4, we execute thePRINT operation and print the data. TheSKIP operation observes that 1 is greater than 0, and so skips the followingSTOP operation. The next operation is aFLIP which exchanges the 0 and 1. Now after jumping back to 4 and printing the data again, theSKIP test fails and the program terminates.
The expected output of the program is:
0 11 0Along withpancake.h, a samplemain.c file is provided in/c/cs223/Hwk3 that reads the contents ofmemory fromstdin, runsrunPancakeProgram, and prints a checksum of the contents ofmemory afterrunPancakeProgram returns.
You can also run the public test script on the copy ofpancake.c in your current directory with the command/c/cs223/Hwk3/public, or on your submitted file with/c/cs223/bin/testit 3 public. Test inputs and outputs are found in the directory/c/cs223/Hwk3/testFiles.
The public test script runs a few tests usingvalgrind, which may catch errors that are not otherwise obvious. You can runvalgrind yourself with the same parameters using the/c/cs223/bin/vg script, as in/c/cs223/bin/vg ./main < something.in.
Submit your implementation ofrunPackageProgram in a filepancake.c using/c/cs223/bin/submit 3 pancake.c. You do not need to submit any other files.
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include"pancake.h"// table of instruction lengths// including arguments// (used to move to next instruction, or to skip)staticconstunsignedint InstructionLength[] = {3,3,2,3,1 };// centralize handling STOP variantsstaticunsignedintnormalize(unsignedint instruction){if(instruction > STOP) { instruction = STOP; }return instruction;}// reverse an arraystaticvoidflip(unsignedint *a,unsignedint n){// special case to avoid n-1 issuesif(n ==0) {return; }unsignedint i =0;unsignedint j = n-1;unsignedint temp;while(i < j) {// swap a[i] and a[j] temp = a[i]; a[i] = a[j]; a[j] = temp;// and update indices i++; j--; }}// print an arraystaticvoidprint(constunsignedint *a,unsignedint n){// special case for n == 0if(n ==0) { putchar('\n'); }else {for(unsignedint i =0; i < n; i++) { printf("%u%c", a[i], (i < n-1) ?' ' :'\n'); } }}// execute a program on a given stack of pancakesvoidrunPancakeProgram(unsignedint *memory){unsignedint pc =0;// program counterunsignedint next;// next instructionunsignedint instruction;// normalized instructionunsignedint x =0;unsignedint y =0;for(;;) { instruction = normalize(memory[pc]);// compute next pc value next = pc + InstructionLength[instruction];// collect arguments// note this only handles 1, 2, and 3switch(InstructionLength[instruction]) {case3: y = memory[pc+2];// fall throughcase2: x = memory[pc+1];// fall throughcase1:break;default: abort();// not handledbreak; }#ifdef TRACE_EXECUTION fprintf(stderr,"# %u: %u %u %u\n", pc, instruction, x, y);#endif// TRACE_EXECUTION// carry out instructionswitch(instruction) {case FLIP: assert(x <= y); flip(memory + x, y - x);break;case PRINT: assert(x <= y); print(memory + x, y - x);break;case JUMP: next = x;break;case SKIP:if(memory[x] > memory[y]) {// add length of next instruction next += InstructionLength[normalize(memory[next])]; }break;case STOP:return;default: abort();// shouldn't happenbreak; } pc = next; }}For this assignment, you are to write a programreverseLetters.c that reads a document fromstdin, and prints tostdout the result of reversing the positions of all the letters, and only the letters, while preserving the case of the letter that appears in each position.
For example, if the input is:
My name is Ozymandias, King of Kings;Look on my Works, ye Mighty, and despair!then the output should be:
Ri apse dd Naythgimey, Skro wy Mnoko;Olsg ni kf Ognik, sa Idnamy, zos iemanym!You may find it helpful to use the various macros defined in the system include filectype.h. For the purpose of this assignment, a value of typechar is a letter if theisalpha macro returns true. A letter is uppercase ifisupper returns true and lowercase ifislower returns true. The macrostoupper andtolower can be used to convert lowercase letters to uppercase and vice versa.
The behavior of these macros may depend on your locale setting. Normally this should not be an issue, but the test script will run withLC_ALL=C, which forces the uppercase letters to be precisely'A' through'Z' and the lowercase letters to be precisely'a' through'z'. If you have set your locale to something else, you can get behavior consistent with the test script by running it like
LC_ALL=C ./reverseLetters < input-file > output-fileYour code will not be tested with any locale other thanC, but if you use thectype macros, you will not need to depend on this assumption.
Non-letters include allchar values for whichisalpha returns 0. Note that this includes the null character'\0', which may appear in the input.
Sample inputs and corresponding outputs can be found in/c/cs223/Hwk4. You can run the public test script on your submitted file using/c/cs223/bin/testit 4 public and on thereverseLetters.c file in your current directory using/c/cs223/Hwk4/test.public.
Some of the sample inputs may be famous works of classic (that is, out of copyright) literature. Parsing the reversed output too carefully may spoil surprises in the endings, so be careful if you don’t want to find out that the whale was really Captain Ahab’s sled.
Submit a filereverseLetters.c that includes amain routine and compiles to a program that meets the above specification, using the usual command
/c/cs223/bin/submit 4 reverseLetters.c#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include<ctype.h>// reverse all the letters in stdin, preserving case// and position of non-letters//// "Hi there!" -> "Re ehtih!"// initial buffer size for getInput#define INITIAL_SIZE (1024)#define SIZE_MULTIPLIER (2)// read stdin until EOF// *n gets number of characters read// return value is pointer malloc'd bytes// (caller must free)// Note: allows binary data, so no null terminator!char *getInput(size_t *n){size_t size = INITIAL_SIZE;// space allocated to bufchar *buf = malloc(size);int c; assert(buf); *n =0;while((c = getchar()) != EOF) {if(*n >= size) { size *= SIZE_MULTIPLIER; buf = realloc(buf, size); assert(buf); } buf[(*n)++] = c; } buf = realloc(buf, *n);return buf;}// return c with case of referencecharmatchCase(char reference,char c){if(isupper(reference) && islower(c)) { c = toupper(c); }elseif(islower(reference) && isupper(c)) { c = tolower(c); }return c;}intmain(int argc,char **argv){if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; }// read everythingsize_t n;char *s = getInput(&n);// algorithm://// i is next position to print, starts at 0// j is upper bound on next alpha character, starts a n-1//// when s[i] is an alpha char, move j down until it hits an alpha,// then print s[j] (with case swapped as needed) insteadsize_t i =0;size_t j = n-1;for(; i < n; i++) {if(isalpha(s[i])) {// find actual character to printwhile(!isalpha(s[j])) { j--; } putchar(matchCase(s[i], s[j])); j--; }else {// just print it putchar(s[i]); } } free(s);return0;}For this assignment, you are to implement an array-like data structure that storesints at positions indexed by fixed-length strings of lowercase letters. The interface is given by the filewordArray.h below:
// interface for arrays keyed on fixed-length strings// of lowercase letterstypedefstruct wordArray *WordArray;// some constants for key elements#define MIN_KEY_LETTER ('a')#define MAX_KEY_LETTER ('z')#define NUM_KEY_LETTERS (MAX_KEY_LETTER - MIN_KEY_LETTER + 1)// create an array indexed by strings of length nWordArray wordArrayCreate(unsignedint n);// free all space used by arrayvoid wordArrayDestroy(WordArray w);// get the key length n for an arrayint wordArrayKeyLength(WordArray w);// Returns a pointer to location in word array// indexed by string key.//// If key is the wrong length, or contains// characters that are not between MIN_KEY and MAX_KEY,// returns 0.int *wordArrayRef(WordArray w,constchar *key);// returns malloc'd minimum key for w// as a null-terminated stringchar *wordArrayMinKey(WordArray w);// Increments a string in place, returning 1 on overflow// and 0 otherwise//// Examples://// incKey("aaa") -> "aab"// incKey("abc") -> "abd"// incKey("abz") -> "aca"// incKey("zzz") -> "aaa", returns 1int wordArrayIncKey(char *s);// print the contents of an array,// with one key and value on each line, as in//// aa 0// ab 0// ac 0// ...//void wordArrayPrint(WordArray w);AWordArray object is allocated usingwordArrayCreate and deallocated usingwordArrayDestroy. When initially allocated, the value at each position in theWordArray should be 0. ThewordArrayCreate function takes a key length as its only parameter. This controls the length of keys for accessing theWordArray, and can be recovered with thewordArrayKeyLength function.
The primary function for accessing the array iswordArrayRef, which takes aWordArray and a key as an argument and returns a pointer to the corresponding location. An typical use case might look like this:
WordArray w = wordArrayCreate(3); *wordArrayRef(w,"abc") =12; printf("%d\n", *wordArrayRef(w,"abc"));The smallest key is an all-a string, and the largest is an all-z string. You should provide a functionwordArrayMinKey that returns a freshly-malloc’d string ofa characters of the appropriate length for a givenWordArray, and awordArrayIncKey that computes the next key after a given key, returning 0 if this does not result in overflow and 1 if it would (because the input key is an all-z string). This function should update its argument in place, with the effect on the argument in the case of overflow unspecified. The intent is that a caller may iterate over all the keys in aWordArrayw using a loop like this:
You may find this pattern helpful for implementing the last remaining function,wordArrayPrint, which prints all keys of aWordArray in order, one per line, each followed by a space and its corresponding value.
Several small test programs, along with the header filewordArray.h, can be found in/c/cs223/Hwk5, and can also bedownloaded here. These can be compiled using theMakefile provided in that directory. As usual, you can also call the public test script/c/cs223/Hwk5/test.public either directly, or on your submitted assignment with/c/cs223/bin/testit 5 public.
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include<string.h>#include"wordArray.h"struct wordArray {int n;// key lengthint *contents;// data in row-major order};// create an array indexed by strings of length nWordArraywordArrayCreate(unsignedint n){// compute number of elements NUM_KEY_LETTERS**nsize_t size =1;for(unsignedint i =0; i < n; i++) { size *= NUM_KEY_LETTERS; } WordArray w = malloc(sizeof(struct wordArray)); assert(w); w->n = n; w->contents = calloc(size,sizeof(int)); assert(w->contents);return w;}// free all space used by arrayvoidwordArrayDestroy(WordArray w){ free(w->contents); free(w);}// return key length for wintwordArrayKeyLength(const WordArray w){return w->n;}// Returns a pointer to location in word array// indexed by string key.//// If key is the wrong length or contains// characters that are not lowercase letters,// returns 0.int *wordArrayRef(WordArray w,constchar *key){size_t index =0;if(strlen(key) != wordArrayKeyLength(w)) {// wrong lengthreturn0; }for(int i =0; i < w->n; i++) {if(key[i] < MIN_KEY_LETTER || key[i] > MAX_KEY_LETTER) {// bounds check failed!return0; }else {// shift previous index and add new letter index = index * NUM_KEY_LETTERS + (key[i] - MIN_KEY_LETTER); } }return &w->contents[index];}// returns malloc'd minimum key for w// as a null-terminated stringchar *wordArrayMinKey(const WordArray w){int n = wordArrayKeyLength(w);// +1 for nullchar *s = malloc(n+1); assert(s);for(int i =0; i < n; i++) { s[i] = MIN_KEY_LETTER; } s[n] = '\0';return s;}// Increments a null-terminated string in place, returning 1 on overflow// and 0 otherwise//// Examples://// incKey("aaa") -> "aab"// incKey("abc") -> "abd"// incKey("abz") -> "aca"// incKey("zzz") -> "aaa", returns 1intwordArrayIncKey(char *s){size_t n = strlen(s);if(n ==0) {// 0 length strings always overflowreturn1; }// if we got here, we know there is at// least one charactersize_t i = n-1;for(;;) {// increment position iif(s[i] == MAX_KEY_LETTER) { s[i] = MIN_KEY_LETTER; }else { s[i]++;// done, no overflowreturn0; }// need to move on to next positionif(i ==0) {// there is no next positionreturn1; }else { i--; } }}// print the contents of a wordArrayvoidwordArrayPrint(WordArray w){char *key = wordArrayMinKey(w);do { printf("%s %d\n", key, *wordArrayRef(w, key)); }while(wordArrayIncKey(key) ==0); free(key);}Submit the filewordArray.c as usual with/c/cs223/bin/submit 5 wordArray.c. You do not need to submit any other files.
For this assignment, you are to implement a data structure that represents a editable buffer in a text editor. The contents of the buffer, conceptually, are a string of zero or more non-null characters, followed by a null, although the actual representation might be different. When the buffer is created, the user specifies that it should have zero or more cursors, indexes into the buffer that can be used as locations to insert new characters. Inserting a character does not change what character each cursor points to, but functions are provided to move cursors forward or backward within the buffer.
For example, in the picture below, the bottom line represents the text in the buffer while the numbers on the top line represent the location of three cursors numbered 0, 1, and 2:
0 2 1abcdefghijIf we insert anX before cursor 2, the new state will look like this:
0 2 1abcdeXfghijWe can now call a function to move cursor 2 back one position, to put it on theX:
0 2 1abcdeXfghijIf we now insert aY before cursor 0, we get this:
0 2 1abYcdeXfghijBy continuing to insert new characters, and possibly moving the cursors around, we can generate a string of arbitrary length. This string can then be extracted as a standard C string in a malloc’d block using an accessor function.
The full interface is given in the file/c/cs223/Hwk6/buffer.h, reproduced below.
// Abstract data type for an editable string buffer.//// A buffer is a sequence of zero or more non-null characters// followed by a null.//// It also tracks n cursor positions, each of which is a pointer// to some character.//// Initially, the buffer is empty, and all cursors point to the null.//// For the functions bufferInsert, bufferCursorForward,// and BufferCursorBack, specifying a cursor index that is// too large results in the function having no effect.typedefstruct buffer *Buffer;// Make a buffer with n cursors.Buffer bufferCreate(size_t n);// Free all space used by a buffer.void bufferDestroy(Buffer b);// Insert a new non-null character before the i-th cursor position.// All cursors continue to point to the same character// they pointed to before.// If i is out of range or c is '\0', has no effect.void bufferInsert(Buffer b,size_t i,char c);// Advance cursor i one position.// If i is out of range, or the i-th cursor// already points to the final '\0',// has no effect.void bufferCursorForward(Buffer b,size_t i);// Move cursor i back one position.// If i is out of range, or the i-th cursor// already points to the first character,// has no effect.void bufferCursorBack(Buffer b,size_t i);// Return the number of cursors in a buffersize_t bufferCursors(Buffer b);// Return the number of characters in a buffer,// not including the final null.size_t bufferSize(Buffer b);// Return the characters in a buffer// as a null-terminated sequence of chars.// Return value is malloc'd and should be freed by caller.char *bufferContents(Buffer b);The same directory also includes a sampleMakefile; amain program for a simple text editor that compiles against an implementation of the buffer data type in a filebuffer.c that you are asked to provide; and afuzz tester that generates consistent pseudorandom input for the editor.
Some sample inputs and outputs for the editor are provided in the directory/c/cs223/Hwk6/testFiles. The number at the start of each input file name specifies what argument to give toeditor to get the corresponding output. The public test script/c/cs223/Hwk6/test.public will run these tests (and a few valgrind tests) on thebuffer.c file in your current directory. You can also use/c/cs223/bin/testit 6 public to run this script on your submitted file.
Submit your filebuffer.c as usual with/c/cs223/bin/submit 6 buffer.c. You do not need to submit any other files.
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include"buffer.h"struct buffer {struct elt *terminator;// this always points to the null terminatorsize_t n;// number of cursorssize_t size;// number of non-null charactersstruct elt *cursors[];// array of cursor pointers};// we store the buffer as a circular doubly-linked liststruct elt {struct elt *next;struct elt *prev;char c;};BufferbufferCreate(size_t n){ Buffer b = malloc(sizeof(struct buffer) + n *sizeof(struct elt *)); assert(b); b->n = n; b->size =0; b->terminator = malloc(sizeof(struct elt)); assert(b->terminator); b->terminator->next = b->terminator->prev = b->terminator;for(size_t i =0; i < n; i++) { b->cursors[i] = b->terminator; }return b;}voidbufferDestroy(Buffer b){struct elt *e;struct elt *next; next = b->terminator->next;do { e = next; next = e->next; free(e); }while(e != b->terminator); free(b);}voidbufferInsert(Buffer b,size_t i,char c){if(i >= b->n || c == '\0') {return; }// elsestruct elt *e = malloc(sizeof(struct elt)); e->c = c; e->next = b->cursors[i]; e->prev = b->cursors[i]->prev;// we could write this in terms of b->cursors[i]// but this is more symmetric e->next->prev = e; e->prev->next = e;// maintain size invariant b->size++;}voidbufferCursorForward(Buffer b,size_t i){if(i >= b->n || b->cursors[i] == b->terminator) {return; }else { b->cursors[i] = b->cursors[i]->next; }}voidbufferCursorBack(Buffer b,size_t i){if(i >= b->n || b->cursors[i] == b->terminator->next) {return; }else { b->cursors[i] = b->cursors[i]->prev; }}size_tbufferCursors(Buffer b){return b->n;}size_tbufferSize(Buffer b){return b->size;}char *bufferContents(Buffer b){char *output = malloc(bufferSize(b) +1); assert(output);char *top = output;for(struct elt *e = b->terminator->next; e != b->terminator; e = e->next) { *top++ = e->c; } *top++ = '\0';return output;}For this assignment, you are to implement a toy password cracker.
The provided programencrypt.c encrypts a password file provided onstdin. Each line of the password file contains a username and a password, separated by a':' character. Theencrypt.c program hashes each password through the FNV-1a algorithm and prints the resulting 64-bit hash as 16 hexadecimal characters. To allow different encrypted password files to use a different mapping, asalt, provided inargv[1], is prepended to each password before it is hashed.
For example, suppose we are given this password file:
The result of running./encrypt salt < turtles.passwords is
raphael:4ccbdc0ec01f2edcmichaelangelo:4ccbdc0ec01f2edcleonardo:9d95da1074fcbcb7donatallelo:ba29395850adc9a4splinter:7499c221e9c82291Some of these encrypted passwords are the result of applying the hash function to the string"saltpassword", although a few of the users are marginally smarter about picking passwords.
Many of the passwords in the sample file above are easily guessed. If we know the salt and have a dictionary of likely passwords, we can recover the original passwords (or at least passwords that hash to the same value), by running all of the passwords in the dictionary through the hash function with the given salt, and looking for matches in the file of encrypted passwords.
Your task is to write a programdecrypt that will take an encrypted password file in the above format fromstdin, a salt value fromargv[1], and the name of a dictionary file containing one password per line fromargv[2]; and produce onstdout a decrypted version of the file. In the case where a password cannot be recovered, you should print the encrypted version as-is. A typical execution, using the dictionarypasswords.dict might look like this:
$ ./encrypt salt < turtles.passwords | ./decrypt salt passwords.dictraphael:passwordmichaelangelo:passwordleonardo:password1donatallelo:password123splinter:7499c221e9c82291Here the program successfully recovered four of the five passwords, but was defeated by Splinter-sensei’s clever use of Google Translate.
Your program should run efficiently even with large dictionaries and large password files.
You should feel free useencrypt.c as a starting point fordecrypt.c, as well as use any code examples that might be helpful from the notes or lectures. Any such code re-use should be documented in the interest of both academic honesty and as an aid to your own memory if you later need to figure out where it came from.
Encrypted passwords are formatted usingprintf with thePRIx64 macro frominttypes.h. The easiest way to read them back is by usingscanf with the correspondingSCNx64 macro, like this:
decrypt when given a malformed password or dictionary file is undefined. So you can assume that the encrypted password file is always generated by runningencrypt.c on an input with no null characters and a delimiter on each line; and that the dictionary file always contains one password per line, also with no null characters.For this assignment, you can turn in whatever.c or.h files you like, but you must include aMakefile that generates thedecrypt program from these files whenmake is called with no arguments. These files should be submitted as usual using/c/cs223/bin/submit 7filenames.
A public test script is provided in/c/cs223/Hwk7/test.public. This will run on the source files in your current directory. You can also run this script on your submitted files using/c/cs223/bin/testit 7 public.
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include<inttypes.h>#include"table.h"// Password cracker.//// Takes a salt string from argv[1], the name of a file with// passwords to try from argv[2],// and an encrypted password file with lines of the form//// username:password//// from stdin, and sends to stdout out an decrypted password// file with lines of the form//// username:decrypted-password//// Passwords are decrypted by looking for matching lines in the// file specified in argv[2]. If a password cannot be decrypted,// it is passed through unmodified.// All the hash stuff is copied from encrypt.c#define FNV_PRIME_64 ((1ULL<<40)+(1<<8)+0xb3)#define FNV_OFFSET_BASIS_64 (14695981039346656037ULL)// modified version of FNV1a that updates hash// based on string sstaticvoidFNV1a(uint64_t *hash,constchar *s){while(*s) { *hash ^= *s++; *hash *= FNV_PRIME_64; }}// precompute result of applying saltstaticuint64_tprecomputeSaltHash(constchar *salt){uint64_t hash = FNV_OFFSET_BASIS_64; FNV1a(&hash, salt);return hash;}// hash string with precomputed saltstaticuint64_tpasswordHash(uint64_t saltHash,constchar *password){ FNV1a(&saltHash, password);return saltHash;}#define GETLINE_INITIAL_SIZE (16)#define GETLINE_MULTIPLIER (2)// fetch characters from f to first newline or EOL// returns malloc'd buffer that should be freed by caller// if EOL is first character, returns 0staticchar *getLine(FILE *f){int c;// invariant:// size > top// size = size of buffersize_t size = GETLINE_INITIAL_SIZE;size_t top =0;char *buffer = calloc(size,sizeof(char)); assert(buffer);for(;;) {switch((c = getc(f))) {case EOF:if(top ==0) {// got nothing free(buffer);return0; }// else fall throughcase'\n': buffer[top] = '\0';return buffer;default: buffer[top++] = c;if(top >= size) { size *= GETLINE_MULTIPLIER; buffer = realloc(buffer, size *sizeof(char)); assert(buffer); } } }}#define PASSWORD_DELIMITER (':')intmain(int argc,char **argv){if(argc !=3) { fprintf(stderr,"Usage: %s salt dictionary < encrypted-password-file > decrypted-password-file\n", argv[0]);return1; }// precompute saltuint64_t saltHash = precomputeSaltHash(argv[1]);int c;char *password;// from inputconstchar *found;// from tableuint64_t hash;// generate table of hashed passwords Table t = tableCreate(); assert(t);FILE *f = fopen(argv[2],"r");if(f ==0) { perror(argv[2]);return1; }while((password = getLine(f)) !=0) { tableInsert(t, passwordHash(saltHash, password), password); free(password); } fclose(f);// main loop: copy username, then attempt to decrypt passwordwhile((c = getchar()) != EOF) {// copy to stdout putchar(c);if(c == PASSWORD_DELIMITER) {// process passwordif(scanf("%" SCNx64"\n", &hash) !=1) {// bad formatreturn2; }if((found = tableLookup(t, hash)) !=0) { puts(found); }else {// reprint the hashed password printf("%" PRIx64"\n", hash); } } } tableDestroy(t);return0;}// hash table for passwordstypedefstruct table *Table;Table tableCreate(void);void tableDestroy(Table);// insert string with given hashvoid tableInsert(Table,uint64_t hash,constchar *string);// Find string from hash// returns 0 if not found, otherwise pointer// to internal string.// It is safe to use this string until// you call tableInsert or tableDestroy.constchar *tableLookup(Table,uint64_t hash);#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include<string.h>#include"table.h"// adapted from dictStringInt.c example from lecturestruct table {size_t m;// number of locations in tablesize_t n;// number of stored elementsstruct elt **table;// table of linked lists};struct elt {struct elt *next;uint64_t hash;// hash of passwordchar password[];// null-terminated character string};#define INITIAL_SIZE (32)#define SIZE_MULTIPLIER (2)static TabletableCreateInternal(size_t initialSize){ Table d = malloc(sizeof(struct table)); assert(d); d->m = initialSize; d->n =0; d->table = calloc(d->m,sizeof(struct elt *)); assert(d->table);return d;}TabletableCreate(void){return tableCreateInternal(INITIAL_SIZE);}voidtableDestroy(Table d){// walk through all table entries and delete themstruct elt *next;for(size_t i =0; i < d->m; i++) {for(struct elt *e = d->table[i]; e !=0; e = next) { next = e->next; free(e); } } free(d->table); free(d);}// returns pointer to string with given hashconstchar *tableLookup(Table d,uint64_t hash){// where to start lookingsize_t i = hash % d->m;// walk down linked listfor(struct elt *e = d->table[i]; e !=0; e = e->next) {if(e->hash == hash) {// found itreturn e->password; } }// didn't find itreturn0;}// insert new hash/password pairvoidtableInsert(Table d,uint64_t hash,constchar *password){size_t i = hash % d->m;// make a new elt, patch instruct elt *e = malloc(sizeof(struct elt) + strlen(password) +1); e->next = d->table[i]; e->hash = hash; strcpy(e->password, password); d->table[i] = e; d->n++;if(d->n >= d->m) {// too full!// make a new table Table d2 = tableCreateInternal(d->m * SIZE_MULTIPLIER);// reinsert everythingfor(size_t i =0; i < d->m; i++) {for(struct elt *e = d->table[i]; e !=0; e = e->next) { tableInsert(d2, e->hash, e->password); } }// swap guts of d and d2struct table temp; temp = *d; *d = *d2; *d2 = temp;// delete d2 tableDestroy(d2); }}CC=c99CFLAGS=-g3 -WallPROGRAMS=decryptall:$(PROGRAMS)decrypt: decrypt.o table.odecrypt.o: decrypt.c table.htable.o: table.c table.hclean:$(RM) *.o$(PROGRAMS)For this assignment, you are to implement a simple bigram-based data compressor.
The idea is that we will replace frequent pairs of characters (orbigrams) with single characters, where the single characters are chosen from a range not likely to appear in the input. If the input is long enough and has a lot of frequent bigrams, this should reduce its length even taking into account the need to write down which bigram each compressed character expands to.
We also have to deal with the issue of a compressed character showing up in the input, without being part of a frequent bigram. To distinguish this case from an actual compressed character, a specialescape character is placed in the output before the real character. The escape character is also used to escape (uncompressed) escape characters. This means that any compressed character or escape character, if it is not replaced by a single character as part of a bigram, will take two bytes in the output, so input files with a lot of characters in this range may actually increase in size when compressed.
For this assignment, we will use exactly 127 compressed characters, those in the range0x80 through0xfe. These are defined for convenience asCOMPRESSED_CHAR_MIN andCOMPRESSED_CHAR_MAX in the provided filecompressionFormat.h, which also defines some other constants you may find useful. The character0xff orCOMPRESSED_CHAR_ESCAPE will be used as the escape character.
As an example of compression, suppose the input is the stringbanana. Here we might choose to compressan as0x80, giving an output sequenceb 0x80 0x80 a. However, we have to prefix this with a dictionary that lists the expansions of all 127 compressed characters in order. This will start withba for0x80, followed by 126 other expansions, giving a total compressed length of 254 + 4 = 258 bytes for a 6-byte input string. This is terrible, but we can hope for better performance on longer inputs.
If we have non-ASCII data in our input, we may have to escape some characters. For example, if the input characters are0x80 0xff 0xc2, and none of these characters are covered by bigrams in the dictionary, the output will be0xff 0x80 0xff 0xff 0xff 0xc2, since we have to prefix each byte withCOMPRESSED_CHAR_ESCAPE. This raises the length from 3 bytes to 6 bytes, even before adding 254 bytes for the dictionary, but again it’s a short input.
Because we have no way of signaling unused compressed characters, the dictionary will always be 254 bytes long. Each consecutive pair of bytes represents the expansion of a single compressed character. For example, if we choose to compress the stringbanana by assigning0x80 toba,0x81 toan,0x82 tona, and the remaining characters toxy, then the dictionary would start withbaannaxyxyxy… [121 copies ofxy omitted], after which the compresed text might be represented as0x80 0x82 0x82`.
There is a lot of flexibility in how we choose the dictionary and how we choose which overlapping pairs of compressible bigrams to compress. Ideally, we do whatever gives the smallest possible output file. Doing this perfectly is tricky: for example, when compressingbanana, if we compressan, then compressingna is redundant, and on a longer version of this example we might be able to use an extra compressed character to profit somewhere else. However, just picking the 127 most frequent bigrams in the input without worrying about overlap should work well enough for most inputs. The test script used to grade your compressor will accept any compressed file that produces the correct output and that gets a compression ratio that is not substantially worse than a simple application of this greedy strategy achieves.
We have supplied source code for a decompressordecompress.c that you can use to test the output of your compressor. This is also available in compiled form in the Zoo as an executable/c/cs223/Hwk8/decompress.
This you get to supply. You can submit any.c or.h source files you like, but you must submit aMakefile that produces an executablecompress by runningmake compress. Likedecompress, this should take input fromstdin and send output tostdout. Files should be submitted as usual with/c/cs223/bin/submit 8filenames.
You can use the provided shell scripttestCompress to run yourcompress executable on an arbitrary file (provided as a filename argument to the script), check its output for correctness, and compute an approximate compression ratio as a percentage if the output is correct. This script works on the Zoo assuming the current directory contains bothcompress anddecompress executables.
The public test script/c/cs223/Hwk8/test.public will compilecompress based on the files in your current directory, and runtestCompress on sample files from/c/cs223/Hwk8/testFiles. You can also run this on your submitted files using/c/cs223/bin/testit 8.
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include<string.h>#include<limits.h>#include"compressionFormat.h"#define READ_ALL_BUFFER_INITIAL (1024)#define READ_ALL_MULTIPLIER (2)// Return all characters from stream// as malloc'd string.//// Number of characters read is put in *n.staticunsignedchar *readAll(FILE *f,size_t *n){size_t size = READ_ALL_BUFFER_INITIAL;unsignedchar *buffer = malloc(size); assert(buffer); *n =0;int c;while((c = getc(f)) != EOF) {// grow buffer if neededif(*n >= size) { size *= READ_ALL_MULTIPLIER; buffer = realloc(buffer, size); assert(buffer); } buffer[(*n)++] = c; }return buffer;}struct count {unsignedchar expansion[COMPRESSED_EXPANSION];size_t count;};// Compare two struct counts by count fields.// Bigger value sorts earlierstaticintcountCompare(constvoid *a,constvoid *b){return ((struct count *) b)->count - ((struct count *) a)->count;}intmain(int argc,char **argv){if(argc !=1) { fprintf(stderr,"Usage: %s < uncompressed-data > compressed-data\n", argv[0]);return1; }// grab entire contents of input// since we are going to need two passessize_t n;unsignedchar *s = readAll(stdin, &n);// build a table of bigram counts// that we can sort// all of this assumes COMPRESSED_EXPANSION == 2struct count counts[UCHAR_MAX+1][UCHAR_MAX+1];for(int i =0; i <= UCHAR_MAX; i++) {for(int j =0; j <= UCHAR_MAX; j++) { counts[i][j].expansion[0] = i; counts[i][j].expansion[1] = j; counts[i][j].count =0; } }for(size_t i =0; i +1 < n; i++) {// There is an optimization here we are not doing.// Compressing bigrams that contain high-bit characters// is worth more than bigrams with low-bit characters,// since we also avoid the escape. But this works reasonably// well and I don't want to set the par values too low. counts[s[i]][s[i+1]].count++; }// flattened version of countsstruct count *flatCounts = (struct count *) counts; qsort(flatCounts, (UCHAR_MAX+1)*(UCHAR_MAX+1),sizeof(struct count), countCompare);// another table to make things fastunsignedchar compression[UCHAR_MAX+1][UCHAR_MAX+1];for(int i =0; i <= UCHAR_MAX; i++) {for(int j =0; j <= UCHAR_MAX; j++) { compression[i][j] =0; } }// first COMPRESSED_CHARS entries get compressedfor(int i =0; i < COMPRESSED_CHARS; i++) { compression[flatCounts[i].expansion[0]][flatCounts[i].expansion[1]] = COMPRESSED_CHAR_MIN + i;for(int j =0; j < COMPRESSED_EXPANSION; j++) { putchar(flatCounts[i].expansion[j]); } }// now process the input looking for compressible// and escape-needing charactersfor(unsignedchar *p = s; p < s + n; p++) {if(p +1 < s + n && compression[p[0]][p[1]]) {// can compress putchar(compression[p[0]][p[1]]);// eat extra character p++; }elseif(*p >= COMPRESSED_CHAR_MIN) {// must escape putchar(COMPRESSED_CHAR_ESCAPE); putchar(*p); }else {// copy through putchar(*p); } } free(s);return0;}CC=c99CFLAGS=-g3 -WallPROGRAMS=decompress compressall:$(PROGRAMS)decompress: decompress.odecompress.o: decompress.c compressionFormat.hcompress: compress.ocompress.o: compress.c compressionFormat.hclean:$(RM) *.o$(PROGRAMS)For this assignment, you are to implement a program that converts files in a particular markup language to plain text.
Some languages are written left-to-right, some right-to-left, and a few are written inboth directions. ThePancake Flipper Markup Language orPFML is a markup language designed to allow users to write characters in their preferred direction, and have the ordering reversed as needed for display.
The syntax of PFML is simple: Any characters between a matching pair of delimiters'{' and'}' are reversed on output. This is recursive, so if a{} pair contains another{} pair, the characters inside the inner{} pair will be reversed twice, and in principle we may have arbitrary nesting of{} pairs.
When a PFML text is rendered, the{} characters are omitted. So, for example, the text
{uoy {are} woH} {?yadot}is rendered as
How are you today?This example also shows how{} groups are effectively processed from the bottom up, so that{are}, for example, is rendered asera before being reversed again, rather than being turned into}era{ by the outer pair of braces.
Characters other than{} are passed through unmodified, except for possibly being reordered. Only the'{' and'}' characters have any special meaning in PFML.
Create a programpfml that will take a PFML text as input fromstdin and produce the rendered version as output onstdout. Submit whatever.c and.h files are needed to build this program, as well as aMakefile that will build it when called asmake pfml.
There are many possible ways to implement this program, but the easiest may be to build some sort of recursive descent parser along the lines of the calculator example fromRecursive descent parsing. To allow use of recursion to complete this assignment, the test script used for grading will not include unreasonably deep nesting of{} groups.
You may also assume that inputs arewell-formed: every'{' is balanced by a matching'}', and vice versa. If the input is not well-formed, the behavior ofpfml is undefined.
Submit your assignment as usual using/c/cs223/bin/submit 9filenames.
A public test script is provided in/c/cs223/Hwk9/test.public. You can run this on your submitted files with/c/cs223/bin/testit 9 public. You should also feel free to write new tests for your own use if that would be helpful.
I used a separateBytes data structure, defined inbytes.c andbytes.h to accumulate the output, and handled input using a recursive descent parser defined inpfml.c.
// extensible block of bytes// stored as unsigned charstypedefstruct bytes *Bytes;// create an empty blockBytes bytesCreate(void);// destroy a blockvoid bytesDestroy(Bytes);// append an unsigned charvoid bytesAppendChar(Bytes,unsignedchar);// append a blockvoid bytesAppendBytes(Bytes, Bytes);// get length of blocksize_t bytesLen(Bytes);// get contents of blockunsignedchar *bytesData(Bytes);// write contents to FILE *void bytesWrite(Bytes,FILE *);#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include<string.h>#include"bytes.h"struct bytes {size_t len;// number of stored characterssize_t size;// space in blockunsignedchar *data;};#define BYTES_INITIAL_SIZE (64)#define BYTES_MULTIPLIER (2)// create an empty blockBytesbytesCreate(void){struct bytes *b = malloc(sizeof(struct bytes)); assert(b); b->len =0; b->size = BYTES_INITIAL_SIZE; b->data = malloc(b->size); assert(b->data);return b;}// destroy a blockvoidbytesDestroy(Bytes b){ free(b->data); free(b);}// append an unsigned charvoidbytesAppendChar(Bytes b,unsignedchar c){if(b->len >= b->size) { b->size *= BYTES_MULTIPLIER; b->data = realloc(b->data, b->size); assert(b->data); } b->data[b->len++] = c;}// append a blockvoidbytesAppendBytes(Bytes b, Bytes b2){// since b2 might be big, we will grow to fit// rather than just doublingsize_t newLen = b->len + b2->len;if(newLen > b->size) { b->size = newLen; b->data = realloc(b->data, b->size); assert(b->data); } memcpy(b->data + b->len, b2->data, b2->len); b->len = newLen;}// get length of blocksize_tbytesLen(Bytes b){return b->len;}// get contents of blockunsignedchar *bytesData(Bytes b){return b->data;}// write contents to FILE *voidbytesWrite(Bytes b,FILE *f){ fwrite(b->data,sizeof(unsignedchar), b->len, f);}#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include"bytes.h"// constants for Pancake Flipper Markup Language#define PFML_OPEN_DELIMITER ('{')#define PFML_CLOSE_DELIMITER ('}')// Utilities for flipping strings.// flip all characters in bstaticvoidflipBytes(Bytes b){unsignedchar *start = bytesData(b);unsignedchar *end = bytesData(b) + bytesLen(b) -1;while(start < end) {unsignedchar temp = *start; *start = *end; *end = temp; start++; end--; }}// PFML reader implemented as a recursive descent parser// reads a sequence of characters and groups,// stopping if it reaches PFML_CLOSE_DELIMITER or EOFstatic BytesreadSequence(FILE *f){ Bytes b = bytesCreate(); Bytes b2;// for recursionint c;while((c = getc(f)) != EOF) {switch(c) {case PFML_OPEN_DELIMITER:// recurse and flip b2 = readSequence(f); flipBytes(b2); bytesAppendBytes(b, b2); bytesDestroy(b2);break;case PFML_CLOSE_DELIMITER:goto done;default: bytesAppendChar(b, c);break; } }done:return b;}intmain(int argc,char **argv){if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; } Bytes b = readSequence(stdin); bytesWrite(b, stdout); bytesDestroy(b);return0;}CC=c99CFLAGS=-g3 -WallPROGRAMS=pfmlall:$(PROGRAMS)test: allecho"{uoy {are} woH} {?yadot}" | ./pfmlpfml: pfml.o bytes.opfml.o: pfml.c bytes.hbytes.o: bytes.c bytes.hclean:$(RM) *.o$(PROGRAMS)For this assignment, you are to implement a simplified electronic trading order book.
An order book is typically maintained by a broker or other market maker, and lists pending buy and sell orders from customers. We imagine that each order book object represents buy and sell orders for a single commodity, and that orders are always for one unit of this commodity.
If a customer wishes to buy the commodity, they will issue abuy order represented by a positive integer, which is the price they are willing to pay for the commodity. If instead they wish to sell the commodity, they will issue asell order represented by a negative integer, the price they must be paid to turn over the commodity.
Buy orders and sell orders are kept in the order book until a matching order is found. Two orders match if the sum of the (negative) sell price and the (positive) buy price exceed 0, meaning that the broker can make a profit by connecting the corresponding seller to the buyer.
For this assignment, you do not need to keep track of individual sellers and buyers, only orders. When an order comes in, it should be matched with a previous order that maximizes the profit. If there is no such order, the new order goes onto the order book, awaiting a future match. For example, if the order book contains orders with prices:
-5-5-4-32Then a new order of 5 would match -3 for a profit of 2, even though it could also match -4 for a lower profit of 1. This has the effect of removing the matched order, leaving the new set of prices:
-5-5-42If instead an order of 3 came in, it would be added to the order book, giving this new set of prices:
-5-5-4-323Note that 3 doesnot match -3: the broker will not match orders that yield zero profit.
The interface to anOrderBook object is described in the following header file:
// Order book for high-frequency tradingtypedefstruct orderBook *OrderBook;// Make a new empty order book.OrderBook orderBookCreate(void);// Destroy an order book,// freeing all space// and discarding all pending orders.void orderBookDestroy(OrderBook);// Enter a new order in the order book.//// If price > 0, it represents a buy order.// Return value will be price p2 of sell order// maximizing price + p2 > 0, or 0 if there// is no such sell order.//// If price < 0, it represents a sell order.// Return value will be price p2 of buy order// maximizing price + p2 > 0, or 0 if there// is no such buy order.//// In either of the above cases, if 0 is returned,// then price is entered into the order book// as a pending order available for future matches.// If a nonzero value is returned, the corresponding// pending order is removed.//// If price == 0, no order is entered and// return value is 0.int orderBookInsert(OrderBook,int price);This file is also available as/c/cs223/Hwk10/orderBook.h in the Zoo.
TheorderBookCreate andorderBookDestroy functions are the usual constructor and destructor. These should be implemented to allow an arbitrary number of independentOrderBook objects to exist at the same time.
All order processing goes through theorderBookInsert function. This will return a matching order if one exists and 0 otherwise.
The provided test programtestOrderBook.c, also available as/c/cs223/Hwk10/testOrderBook.c, provides two test functions forOrderBook objects.
When called with one argument, as in./testOrderBook 3, it creates the specified number ofOrderBook objects and processes orders supplied onstdin, where each order specifies the index into the array ofOrderBook objects and a price to be supplied to this object usingorderBookInsert. For each order, it prints the order and its result tostdout.
When called with three arguments, as in./testOrderBook 1337 1000000 10000, a singleOrderBook is created, and a pseudorandom number generator initialized with a seed found inargv[1] is used to generate a sequence of random orders whose number is given byargv[2] and whose maximum value is given byargv[3]. This is useful for testing the suitability of theOrderBook implementation for high-frequency trading.
Your task is to supply an implementation of anOrderBook object as described in theorderBook.h file. You should supply whatever.c or.h files beyondorderBook.h andtestOrderBook.c that are needed to compiletestOrderBook using your implementation, along with aMakefile that generatestestOrderBook when called withmake testOrderBook. You do not need to submitorderBook.h ortestOrderBook.c; these will be supplied by the test script. You may assume thatorderBook.h will be the same as/c/cs223/Hwk10/orderBook.h, but you shouldnot assume thattestOrderBook.c will necessarily be identical to the public version in the final test script, although you may assume that it will not export any identifiers exceptmain so that it will not conflict with any functions in other files.
A public test script is available in/c/cs223/Hwk10/test.public. You can run this on your submitted files using/c/cs223/bin/testit 10 public.
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include"orderBook.h"// implementation will be two heaps,// one for buy orders, one for sell orders.struct maxHeap {size_t size;// size of datasize_t n;// number of elementsint *data;};#define MAXHEAP_INITIAL_SIZE (32)#define MAXHEAP_MULTIPLIER (2)staticstruct maxHeap *maxHeapCreate(void){struct maxHeap *m = malloc(sizeof(struct maxHeap)); assert(m); m->size = MAXHEAP_INITIAL_SIZE; m->n =0; m->data = calloc(m->size,sizeof(int)); assert(m->data);return m;}staticvoidmaxHeapDestroy(struct maxHeap *m){ free(m->data); free(m);}staticsize_tchild(size_t parent,int side){return2*parent +1 + side;}staticsize_tparent(size_t child){return (child -1) /2;}voidswap(int *x,int *y){int temp = *x; *x = *y; *y = temp;}staticintmaxHeapIsEmpty(struct maxHeap *m){return m->n ==0;}staticintmaxHeapMin(struct maxHeap *m){ assert(!maxHeapIsEmpty(m));return m->data[0];}staticvoidmaxHeapInsert(struct maxHeap *m,int value){// expand if neededif(m->n >= m->size) { m->size *= MAXHEAP_MULTIPLIER; m->data = realloc(m->data, m->size *sizeof(int)); assert(m->data); }size_t floater = m->n++; m->data[floater] = value;// are we bigger than parent?while(floater !=0 && m->data[floater] > m->data[parent(floater)]) { swap(&m->data[floater], &m->data[parent(floater)]); floater = parent(floater); }}staticvoidmaxHeapDeleteMin(struct maxHeap *m){if(m->n >0) { swap(&m->data[0], &m->data[--(m->n)]);size_t floater =0;for(;;) {// find he bigger childsize_t bigger = child(floater,0);if(bigger+1 < m->n && m->data[bigger+1] > m->data[bigger]) { bigger = bigger+1; }// is it bigger than floater?if(bigger < m->n && m->data[bigger] > m->data[floater]) {// swap and continue swap(&m->data[floater], &m->data[bigger]); floater = bigger; }else {// no, we are donereturn; } } }}#define SELL (0)#define BUY (1)#define NUM_HEAPS (2)struct orderBook {// We want to find smallest sell order (negative)// and largest buy order (positive).//// In both cases this is a numerical max.//// So we'll use two max heaps,// put negative orders in orders[0],// and put positive orders in orders[1].struct maxHeap *orders[NUM_HEAPS];};OrderBookorderBookCreate(void){ OrderBook b = malloc(sizeof(struct orderBook)); assert(b);for(int i =0; i < NUM_HEAPS; i++) { b->orders[i] = maxHeapCreate(); }return b;}voidorderBookDestroy(OrderBook b){for(int i =0; i < NUM_HEAPS; i++) { maxHeapDestroy(b->orders[i]); } free(b);}intorderBookInsert(OrderBook b,int price){if(price ==0) {// no orderreturn0; }int kind = price >0;// BUY/SELLint match;// Look for best compatible order of opposite kindif(!maxHeapIsEmpty(b->orders[!kind]) && (match = maxHeapMin(b->orders[!kind])) + price >0) {// got a match maxHeapDeleteMin(b->orders[!kind]);return match; }else {// no match maxHeapInsert(b->orders[kind], price);return0; }}CC=c99CFLAGS=-g3 -WallPROGRAMS=testOrderBookall:$(PROGRAMS)test: all(echo"0 -3"; echo"0 4"; echo"0 -5" ; echo"0 4") | ./testOrderBook 2time ./testOrderBook 1337 1000000 1000valgrind -q --leak-check=full ./testOrderBook 1337 1000 20testOrderBook: testOrderBook.o orderBook.otestOrderBook.o: testOrderBook.c orderBook.horderBook.o: orderBook.c orderBook.hclean:$(RM) *.o$(PROGRAMS)For this assignment, your are to build a data structure that manages another data structure.
Inserter data typeOur goal is to build an ordered linked list of null-terminated strings, where the strings are supplied in an arbitrary order. So, for example, if we insert"abc","aaa", and"def", in any order, the resulting list will look like
aaa -> abc -> defDoing this directly can takeΘ(n2) time in the worst case, since we may have to walk all the way down the list for each insertion. So instead, we will build an auxiliaryInserter data structure to manage the insertions for us. By tracking additional information and maintaining pointers to individual elements of the list being constructed, this should allow us to jump directly to the location where a new element needs to be inserted, avoiding theΘ(n) cost of walking the entire list. With an appropriate choice of data structure, we might reasonably hope to get down to as low asO(log n) time to insert a new element into an ordered list ofn elements.
The interface for anInserter is given in the fileinserter.h, also available as/c/cs223/Hwk11/inserter.h in the Zoo.
// An Inserter object manages an ordered linked list// of null-terminated strings.struct element {struct element *next;// next element in listchar *string;// string stored in this element};// Opaque Inserter data typetypedefstruct inserter *Inserter;// Create a new Inserter to manage the list// whose head is stored in *head.// This list should start empty.Inserter inserterCreate(struct element **head);// Add a string to he managed list if not// already present.// The new string will be a malloc'd copy// of s.void inserterAdd(Inserter,constchar *s);// Free all space used by the given Inserter.// This will *not* free any space used by// the managed linked list.void inserterDestroy(Inserter);This file defines astruct element type for the linked list to be constructed. A pointer to the first element of the list (or null if there are no elements) is assumed to be stored in some variable held by the caller. When anInserter object is created usinginserterCreate, a pointer to this variable is passed in. This allows theInserter object to modify the variable as needed while inserting new elements. It is assumed that the list is initially empty; what happens if it is not is undefined.
The main work of theInserter object is done insideinserterAdd. This takes aconst char * pointing to a string to be inserted into the list. If the string is not already in the list, the effect of callinginserterAdd is to insert a newstruct element in the appropriate location in the list, pointing to a malloc’d copy of the supplied string. If the string is already in the list,insertAdd should not change the list. In either case,insertAdd may update whatever internal data structure theInserter object is using to keep track of the list elements.
The remaining functioninserterDestroy should free any space used by theInserter object. It shouldnot free any of thestruct elements in the managed linked list or any of the malloc’d strings that they point to. The intent is that once the list is completely filled in, it should be safe to callinserterDestroy and then continue use the linked list independently.
A test programtestInserter.c is provided. This can also be found in the Zoo in/c/cs223/Hwk11/testInserter.h.
This program has two modes of operation. When called with no arguments, it reads words (separated by whitespace and terminated by EOF) fromstdin and inserts them into a linked list using a singleInserter object, printing the results and cleaning up when done. When called with one argument, it parses this argument to get a number of linked lists andInserter objects to create, then looks for pairs of the formnumberword onstdin, where the number indicates which list to insert the word into. Here are some examples of executions oftestInserter:
$ echo "b a c d g a" | ./testInserter=== 0 ===abcdg$ echo "0 abc 0 aaa 0 def 0 abc 1 qrs" | ./testInserter 3=== 0 ===aaaabcdef=== 1 ===qrs=== 2 ===When compilingtestInserter.c, you may need to be a little careful about compiler flags, because it uses the non-ISO-C99scanf directive%ms to read a word as a new malloc’d string. This will work just fine with most invocations ofgcc, but if you use the-pedantic flag withc99 orgcc -std=c99, you will get warnings.
The test script in/c/cs223/Hwk11/test.public, which can be called on your submitted files with/c/cs223/bin/testit 11 public, will gather up aMakefile and whatever.c and.h files are present in the current directory, supply the standardinserter.h andtestInserter.c, and callmake testInserter using yourMakefile to build thetestInserter program. This will then be run on various test inputs. As usual, the public test script may not precisely reflect the tests that will be used in the final test script to grade your assignment.
You should supply whatever additional.c and.h files are needed to implement yourInserter data type, along with aMakefile that buildstestInserter as described above whenmake is called asmake testInserter. Your files should be submitted using/c/cs223/bin/submit 11filenames. You do not need to submitinserter.h ortestInserter.c, and any submitted files with these names will be overwritten by the test script.
#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include<string.h>#include"inserter.h"#define NUM_KIDS (2)#define LEFT (0)#define RIGHT (1)// We will implement an Inserter using a treap.//// The keys will be the strings in the linked list.// Each tree node will point to a struct element// with the key.struct node {struct element *element;int priority;// for heapstruct node *kids[NUM_KIDS];};// The actual inserter struct contains// a pointer to the root and a pointer// to the list head pointer.struct inserter {struct node *root;struct element **head;};// The rest of this mostly follows the treap// implementation from the 2021-04-19 lecture.InserterinserterCreate(struct element **head){// list must be empty assert(*head ==0); Inserter ins = malloc(sizeof(struct inserter)); assert(ins); ins->root =0; ins->head = head;return ins;}// Free the node in the treestaticvoidtreeDestroy(struct node *root){if(root) {for(int i = LEFT; i <= RIGHT; i++) { treeDestroy(root->kids[i]); } free(root); }}voidinserterDestroy(Inserter ins){ treeDestroy(ins->root); free(ins);}// Find predecessor element to given string.// Returns 0 if none exists.staticstruct element *treeFindPredecessor(struct node *root,constchar *s){if(root ==0) {return0; }elseif(strcmp(root->element->string, s) >=0) {// predecessor is in left subtreereturn treeFindPredecessor(root->kids[LEFT], s); }else {// predecessor is either root->element// or found in right subtreestruct element *pred = treeFindPredecessor(root->kids[RIGHT], s);if(pred) {return pred; }else {return root->element; } }}// Rotate child on side up to root//// x y// A y x C// BC ABstaticvoidtreeRotate(struct node **tree,int side){struct node *x = *tree;struct node *y = (*tree)->kids[side];struct node *b = y->kids[!side];// fix up all the pointers *tree = y; y->kids[!side] = x; x->kids[side] = b;}// Returns new struct element * if not already in tree,// 0 otherwise.staticstruct element *treeInsert(struct node **tree,constchar *s){struct element *e;if(*tree ==0) {// tree is empty e = malloc(sizeof(struct element)); assert(e); e->next =0; e->string = strdup(s);struct node *n = malloc(sizeof(struct node)); assert(n); n->element = e; n->priority = rand(); n->kids[LEFT] = n->kids[RIGHT] =0; *tree = n;return e; }else {int cmp = strcmp((*tree)->element->string, s);if(cmp ==0) {return0; }else {int side = cmp <0; e = treeInsert(&(*tree)->kids[side], s);if((*tree)->priority < (*tree)->kids[side]->priority) { treeRotate(tree, side); }return e; } }}voidinserterAdd(Inserter ins,constchar *s){struct element *e = treeInsert(&ins->root, s);if(e) {struct element *pred = treeFindPredecessor(ins->root, s);if(pred) { e->next = pred->next; pred->next = e; }else { e->next = *(ins->head); *(ins->head) = e; } }}CC=gccCFLAGS=-g3 -WallPROGRAMS=testInserterall:$(PROGRAMS)test: allecho"b a c d g a" | valgrind -q ./testInserterecho"0 abc 0 aaa 0 def 0 abc 1 qrs" | valgrind -q ./testInserter 3testInserter: inserter.o testInserter.oinserter.o: inserter.c inserter.htestInserter.o: testInserter.c inserter.hclean:$(RM) *.o$(PROGRAMS)For this assignment, you are to write a program that computes a distance-2 coloring of an undirected graph.
Adistance-2 coloring of an undirected graphG is an assignment of colors0 throughk − 1 to the vertices ofG, such that any pair of distance-2 neighbors are assigned different colors. For the purpose of this definition, two verticesu andw aredistance-2 neighbors ifu ≠ w and there is a third vertexv such thatuv andvw are both edges inG. Another way of saying this is that a distance-2 neighbor is a neighbor (other than me) of one of my neighbors. This is not quite the same thing as being exactly 2 hops away: we may consideru andw to be distance-2 neighbors even if there is an edge between them that would put them at distance 1.
Note that unlike ordinary graph colorings, it’s permitted for two nodes that have an edge between them to have the same color, so long as they don’t also have a length-2 path between them.
Distance-2 colorings are useful for organizing various sorts of distributed systems. If I can’t tell my neighbors apart otherwise, a distance-2 coloring will distinguish them for me. Or if I have a radio network where my neighbors are the transmitters I can receive from, assigning frequencies based on a distance-2 coloring will prevent the interference that might otherwise occur if two of my neighbors broadcast at the same time.
Finding a distance-2 coloring for a given graph that uses the minimum possible number of colors is known to be an NP-hard problem. However, if we know that a graph has maximum degreed, then every node has at mostd(d − 1) distance-2 neighbors, so usingk = d(d − 1) + 1 colors will always allow us to find a distance-2 coloring simply by coloring each vertex one at a time using some color that has not already been used for one of itsd(d − 1) neighbors-of-neighbors.
You are to write a programcoloring that takes a description of an undirected graph fromstdin, and prints a distance-2 coloring for this graph tostdout. Your coloring should use no more thand(d − 1) + 1 colors, whered is the maximum number of neighbors of any vertex in the graph.
The input format looks like this:
42 01 20 11 3The first number is the numbern of vertices in the graph. Vertices will be numbered from 0 ton − 1.
The remaining lines give the edges of the graph, given as pairs of integer vertex numbers separated by a space. Because the graph is undirected, the order of the vertices in each edge is arbitrary, and the edges themselves are not guaranteed to be arranged in any particular order. You may assume that the edge list contains no duplicates (including reversed copies of edges like0 1 and1 0), that the vertices listed for each edge are in the correct range from0 throughn − 1, and that no edge sends a vertex to itself.
The output of the program should look like this:
0 01 12 23 1The first column lists the vertices in order. The second column gives the color assigned to each vertex as a decimal value. These should be values in the range0 throughd(d − 1), whered is the maximum degree of the graph.
Submit whatever files you need to build your program, along with aMakefile that produces the program whenmake is called withmake coloring, using/c/cs223/bin/submit 12filenames.
We have provided a compiled program/c/cs223/Hwk12/testColoring that will test the output ofcoloring. Typical use is:
./coloring < some-graph | /c/cs223/Hwk12/testColoring some-graphThetestColoring program will complain and return a failure exit status if the coloring provided onstdin does not work for the graph provided in the file named inargv[1]. This may include coloring two neighbors-of-neighbors the same color, using colors outside the allowed range, or simply not having the right format.
A public test script that runstestColoring on some sample graphs is given in/c/cs223/Hwk12/test.public. You can run this on your submitted code with/c/cs223/bin/testit 12 public.
Because thetestColoring program shares a lot of code with the sample solution, we will not be making the source code for this program public until solutions are posted.
I recommend not getting too carried away trying to find the best possible algorithm for distance-2 coloring. An implementation that runs inO(nd2) time with reasonable constants should be fine.
You may assume that bothn andd2 fit in a variable of typesize_t, and that both values are small enough that it is reasonable to allocate an array withn ord2 elements.
Most of the work is done ingraph.c, with header filegraph.h.
The wrapper filescoloring.c andtestColoring.c are used to build the required solutioncoloring and the test programtestColoring. We includetestColoring.c below for completeness, but this was not required for the assignment.
Note that theMakefile only buildscoloring. If you want to buildtestColoring, compiletestColoring.c together withgraph.c.
// Graph data structure for computing distance-2 coloringstypedefstruct graph *Graph;// Read a graph from a FILE *.// Format is n on first line, followed by// edges as pairs of vertices on subsequent lines.// Returns 0 on error.Graph graphRead(FILE *);// Free all space used by a graphvoid graphDestroy(Graph);// Return the number of vertices in a graphsize_t graphSize(Graph);// Compute a distance-2 coloring for a graph g.// Return value is an array of size graphSize(g).size_t *graphDistance2Coloring(Graph g);#define COLORING_OK (0)// coloring works#define COLORING_OUT_OF_RANGE (1)// color out of range#define COLORING_CONFLICT (2)// two neighbors with same color// Check a distance-2 coloring for a graph g.// Returns one of the status codes above.int graphDistance2ColoringCheck(Graph g,size_t *coloring);#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include"graph.h"// Graph data structure for computing distance-2 coloringsstruct edgeList {size_t size;// amount of slots allocated for neighborssize_t degree;// number of edges; may include duplicatessize_t neighbors[];// where the edges go};struct graph {size_t n;// number of verticesstruct edgeList *vertices[];// adjacency lists};#define EDGELIST_INITIAL_SIZE (1)#define EDGELIST_MULTIPLIER (2)staticstruct graph *graphCreate(size_t n){struct graph *g = malloc(sizeof(struct graph) +sizeof(struct edgeList) * n); assert(g); g->n = n;for(size_t i =0; i < n; i++) { g->vertices[i] = malloc(sizeof(struct edgeList) +sizeof(size_t) * EDGELIST_INITIAL_SIZE); assert(g->vertices[i]); g->vertices[i]->size = EDGELIST_INITIAL_SIZE; g->vertices[i]->degree =0; }return g;}size_tgraphSize(Graph g){return g->n;}voidgraphDestroy(Graph g){for(size_t i =0; i < g->n; i++) { free(g->vertices[i]); } free(g);}// add edge from u to vstaticvoidgraphAddDirectedEdge(struct graph *g,size_t u,size_t v){if(g->vertices[u]->degree >= g->vertices[u]->size) { g->vertices[u]->size *= EDGELIST_MULTIPLIER; g->vertices[u] = realloc(g->vertices[u],sizeof(struct edgeList) +sizeof(size_t) * g->vertices[u]->size); assert(g->vertices[u]); } g->vertices[u]->neighbors[g->vertices[u]->degree++] = v;}// add edge from u to v and from v to ustaticvoidgraphAddUndirectedEdge(struct graph *g,size_t u,size_t v){ graphAddDirectedEdge(g, u, v); graphAddDirectedEdge(g, v, u);}// read a graph from stdin// returns 0 on errorstruct graph *graphRead(FILE *f){size_t n;if(fscanf(f,"%zu", &n) !=1) {return0; }// elsestruct graph *g = graphCreate(n);size_t u;size_t v;while(fscanf(f,"%zu%zu", &u, &v) ==2) {// we are promised good input but we'll defend// against bad just in caseif(u <0 || u >= n || v <0 || v >= n || u == v) { graphDestroy(g);return0; } graphAddUndirectedEdge(g, u, v); }return g;}// compute the maximum degree of a graphsize_tgraphDegree(conststruct graph *g){size_t max =0;for(size_t i =0; i < g->n; i++) {if(g->vertices[i]->degree > max) { max = g->vertices[i]->degree; } }return max;}#define NO_COLOR (-1)// Compute a greedy distance-2 coloring of a graph g.// Return value is pointer to a malloc'd array of// exactly g->n size_t values.size_t *graphDistance2Coloring(Graph g){size_t *coloring = calloc(g->n,sizeof(size_t)); assert(coloring);for(size_t i =0; i < g->n; i++) { coloring[i] = NO_COLOR; }// count array will track colors of neighbors of neighborssize_t degree = graphDegree(g);size_t colors = degree * (degree-1) +1;size_t *count = calloc(colors,sizeof(size_t));for(size_t u =0; u < g->n; u++) {// count up all the neighbor-of-neighbor colorsfor(size_t c =0; c < colors; c++) { count[c] =0; }for(size_t i =0; i < g->vertices[u]->degree; i++) {size_t v = g->vertices[u]->neighbors[i];for(size_t j =0; j < g->vertices[v]->degree; j++) {size_t w = g->vertices[v]->neighbors[j];if(coloring[w] != NO_COLOR) { count[coloring[w]]++; } } }// pick first unused colorfor(size_t c =0; c < colors; c++) {if(count[c] ==0) { coloring[u] = c;goto done; } } assert(0);// ran out of colors!done: ; } free(count);return coloring;}// Check an alleged distance 2 coloringintgraphDistance2ColoringCheck(Graph g,size_t *coloring){size_t degree = graphDegree(g);size_t colors = degree * (degree-1) +1;for(size_t u =0; u < g->n; u++) {if(coloring[u] >= colors) { fprintf(stderr,"%zu color %zu exceeds %zu\n", u, coloring[u], colors-1);return COLORING_OUT_OF_RANGE; }// check neighbors of neighbors for conflictsfor(size_t i =0; i < g->vertices[u]->degree; i++) {size_t v = g->vertices[u]->neighbors[i];for(size_t j =0; j < g->vertices[v]->degree; j++) {size_t w = g->vertices[v]->neighbors[j];if(u != w && coloring[u] == coloring[w]) { fprintf(stderr,"%zu-%zu-%zu endpoints have same color %zu\n", u, v, w, coloring[u]);return COLORING_CONFLICT; } } } }return COLORING_OK;}#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>// Compute a distance-2 coloring on a given undirected graph.//// Input format is//// <number of nodes>// <u1> <v1>// <u2> <v2>// ...//// Where <number of nodes>, <ui>, etc. are all %d-formatted ints,// edges are ui-vi in no particular order.//// Output is list of nodes with matching colors in {0..k-1}.#include"graph.h"intmain(int argc,char **argv){if(argc !=1) { fprintf(stderr,"Usage: %s < graph\n", argv[0]);return1; } Graph g = graphRead(stdin); assert(g);size_t *coloring = graphDistance2Coloring(g);size_t n = graphSize(g);for(size_t i =0; i < n; i++) { printf("%zu %zu\n", i, coloring[i]); } free(coloring); graphDestroy(g);return0;}#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<assert.h>#include"graph.h"// check a distance-2 coloring given on stdin// against a graph from file named in argv[1]intmain(int argc,char **argv){if(argc !=2) { fprintf(stderr,"Usage: %s graph < coloring\n", argv[0]);return1; }FILE *f = fopen(argv[1],"r");if(f ==0) { perror(argv[1]);return1; } Graph g = graphRead(f); assert(g); fclose(f);size_t n = graphSize(g);size_t *coloring = calloc(n,sizeof(size_t));for(size_t i =0; i < n; i++) {size_t ii;if(scanf("%zu%zu", &ii, &coloring[i]) !=2 || ii != i) { fprintf(stderr,"Bad coloring format\n");return2; } }switch(graphDistance2ColoringCheck(g, coloring)) {case COLORING_OUT_OF_RANGE:return3;case COLORING_CONFLICT:return4;case COLORING_OK: puts("Good coloring!");break;default: assert(0);// case not handled } free(coloring); graphDestroy(g);return0;}CC=c99CFLAGS=-g3 -WallPROGRAMS=coloringall:$(PROGRAMS)test: allecho"3 0 1 0 2" | ./coloringgraph.o: graph.c graph.hcoloring.o: coloring.c graph.hcoloring: graph.o coloring.o$(CC)$(CFLAGS) -o$@$^clean:$(RM) *.o$(PROGRAMS)Make sure that you sign up for an account on the Zoo athttp://zoo.cs.yale.edu/accounts.html. If you already have an account, you still need to check the CPSC 223 box so that you can turn in assignments. It’s best to do this as soon as possible.
You do not need to develop your solution on the Zoo, but you will need to turn it in there, and it will be tested using the compiler on the Zoo.
For this assignment, you are to implement an encoder forPig Esperanto, a simplified version of the language gamePig Elvish, which in turn is similar toPig Latin.
Pig Esperanto works by translating a text one word at a time. For the purposes of this assignment, a word consists of a consecutive sequence of characters for whichisalpha, defined in the include filectype.h, returns true. Any characters for whichisalpha returns false should be passed through unmodified.
For each input word:
Capitalization can be tested using theisupper andislower macros, and modified using thetoupper andtolower macros. Likeisalpha, these are all defined inctype.h.
You are to write a programencode.c that takes an input fromstdin, encodes it using the above rules, and writes the result tostdout.
For example, given the input
I *REALLY* like Yale's course-selection procedures.Your program should output
Ian *EALLYro* ikelo Aleyo'san ourseco-electionso rocedurespo.Sample inputs and outputs can be found in the directory/c/cs223/Hwk1/testFiles in the Zoo. Note that some of these files contain non-printing characters that may have odd effects if you send them to your screen. The safest way to test if your program produces the same output as the sample output is probably to usecmp, for example:
$ ./encode < test.in > tmp$ cmp tmp test.outIftmp andtest.out contain the same characters,cmp will say nothing. Otherwise it will tell you the first position where they differ.
If you do want to see the characters in a binary file, trying usingod, as in
$ echo hi > foo$ od -t x1 -t c foo0000000 68 69 0a h i \n0000003Submit your assignment using the command:
/c/cs223/bin/submit 1 encode.cYou can test that your program compiles (and passes a few basic tests) using the command:
/c/cs223/bin/testit 1 encodeThe unsympathetic robo-grading script used to grade this assignment may or may not use the same tests as this command, so you should make sure your program works on other inputs as well. You may also want to look at thestyle grading checklist to see that you haven’t committed any gross atrocities against readability, in case a human being should happen to look at your code.
You can submit your assignment more than once, but any late penalties will be assessed based on the last submission. For more details about thesubmit script and its capabilities, seehere.
/* * Translate text into Pig Esperanto, a simplified versoin * of Pig Elvish. */#include<stdio.h>#include<ctype.h>intmain(int argc,char **argv){int c;int firstLetter;/* initial letter if any */int count =0;/* number of letters in the current word so far */for(;;) { c = getchar();if(isalpha(c)) {if(count ==0) {/* first letter */ firstLetter = c; }elseif(count ==1) {/* second letter, fix the case */if(isupper(firstLetter)) { putchar(toupper(c)); }else{ putchar(tolower(c)); } }else {/* just pass it through */ putchar(c); }/* always bump count */ count++; }else {if(count !=0) {/* finish off the previous word */if(count >1) { putchar(tolower(firstLetter)); }else { putchar(firstLetter); }if(count <=3) { putchar('a'); putchar('n'); }else { putchar('o'); } }/* reset count */ count =0;if(c == EOF) {break; }else { putchar(c); } } }return0;}For this assignment, you are to implement a simple transposition cipher.
This cipher encrypts and decrypts a sequence of characters by dividing the sequence into blocks of sizen, wheren is specified by the encryption key. If the input text has a length that is not a multiple ofn, the last block is padded with null characters ('\0').
In addition ton, the key also specifies two parametersa andb. For each block, thei-th output character, starting from0 as usual, is set to thej-th input character, wherej = (ai + b) modn. For appropriate choices ofa andb, this will reorder the characters in the block in a way that can be reversed by choosing a corresponding decryption key(n, a′, b′).
For example, ifn = 5,a = 3, andb = 2, the stringHello, world! would be encrypted like this:
in: H e l l o , w o r l d ! \0 \0i: 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4j: 2 0 3 1 4 2 0 3 1 4 2 0 3 1 4out: l H l e o w , o r ! l \0 d \0Write a programtranspose.c that takesn,a, andb inargv[1],argv[2], andargv[3], respectively, and an input string fromstdin; applies the above encryption; and writes the result tostdout.
You may assume thatn,a, andb are all small enough to fit into variables of typeint. Your program should exit with a nonzero exit code ifn is not at least 1 or if it is not given exactly three arguments, but you do not need to do anything to test for badly-formatted arguments. You should not make any other assumptions about the values ofn,a, orb; for example, either ofa orb could be zero or negative.
Submit your assignment as usual with/c/cs223/bin/submit 2 transpose.c. You can use/c/cs223/bin/testit 2 public to run the public test script on your submitted assignment. You can also run this script in your own directory as/c/cs223/Hwk2/test.public. Sample inputs and outputs used by the test script can be found in/c/cs223/Hwk2/testFiles.
Be aware that null characters in your output will not be visible on most terminals. Usingod -c may be helpful here, since it will explicitly show nulls, spaces, and other non-printing characters. For example:
$ echo -n 'Hello, world!' | ./transpose 5 3 2 | od -c0000000 l H l e o w , o r ! l \0 d \00000017The programcmp may also be helpful for comparing binary files.
/* * Transposition block cipher encoder/decoder. */#include<stdio.h>#include<stdlib.h>#include<assert.h>/* * transpose in to out by the rule * out[i] = in[(a*i+b)%n]; */voidtranspose(constchar *in,char *out,int n,int a,int b){/* we need to do some sneakery to deal with negative remainders */longlong j;for(longlong i =0; i < n; i++) { j = (a * i + b) % n;if(j <0) { j += n; } out[i] = in[j]; }}/* * Send a buffer to stdout. * * Second argument gives length. * * We can't just use fputs because out may contain null characters. */voidship(constchar *out,int n){/* could also use fwrite */for(int i =0; i < n; i++) { putchar(out[i]); }}/* * Read sequence of blocks, feed each to transpose then ship. */intmain(int argc,char **argv){int n;int a;int b;char *in;char *out;int c;int i;if(argc !=4) { fprintf(stderr,"Usage: %s n a b\n", argv[0]);return1; } n = atoi(argv[1]); a = atoi(argv[2]); b = atoi(argv[3]);if(n <=0) { fprintf(stderr,"%s: block size n must be positive\n", argv[0]);return2; } in = malloc(n); assert(in); out = malloc(n); assert(out); i =0;/* more efficient solution would use fread, but EOF is a nuisance */while((c = getchar()) != EOF) {/* write to next position in in */ in[i++] = c;if(i == n) {/* encode and ship */ transpose(in, out, n, a, b); ship(out, n); i =0; } }if(i >0) {/* pad remaining bytes with nulls and ship */for(; i < n; i++) { in[i] = '\0'; } transpose(in, out, n, a, b); ship(out, n); }/* clean up */ free(in); free(out);return0;}For this assignment, you are to implement a data type supporting addition and multiplication of large non-negative integers.
The filenum.h, shown below, defines the interface to the data type. Your job is to provide a matchingnum.c file that implements these functions. You may also implement any other functions that would be helpful, but to be safe it would be best to declare any extra functions static.
ANum represents a possibly very large non-negative integer, and can be initialized by supplying a null-terminated string of ASCII digits to thenumCreate function. You will need to choose an appropriate representation forNums that allows a reasonably efficient implementation of the remaining functions.
A test harness that you can use to try out your code can be found intestNum.c. These files are also available in the directory/c/cs223/Hwk3 on the Zoo.
#ifndef _NUM_H#define _NUM_H#include<stdio.h>/* * High-precision arithmetic on non-negative number in base 10. */typedefstruct num Num;/* constructs a Num from a string *//* string contains representation of the number * in base 10, e.g. numCreate("314159"); * Returns a null pointer (0) if the string contains any non-digits. * Leading zeros are OK: numCreate("012") parses as 12. * Empty string parses as 0 */Num * numCreate(constchar *s);/* Free all resources used by a Num */void numDestroy(Num *n);/* Get the value of the i-th least significant digit of a Num. * Returns 0 if i is out of range. * Example: * n = numCreate("12345"); * numGetDigit(n, 0) == 5 * numGetDigit(n, 3) == 2 * numGetDigit(n, 17) == 0 * numGetDigit(n, -12) == 0 */int numGetDigit(const Num *n,int i);/* add two Nums, returning a new Num *//* does not destroy its inputs, caller must destroy output */Num * numAdd(const Num *x,const Num *y);/* multiply two Nums, returning a new Num *//* does not destroy its inputs, caller must destroy output */Num * numMultiply(const Num *x,const Num *y);/* Print the digits of a number to file. * Do not print any leading zeros unless n is zero. */void numPrint(const Num *n,FILE *f);#endif/* _NUM_H */Submit your assignment as usual with/c/cs223/bin/submit 3 num.c. You can use/c/cs223/bin/testit 3 public to run the public test script on your submitted assignment, which is also available in/c/cs223/Hwk3/test.public.
int for the index innumGetDigit, although in principlenumPrint could extract more digits than this. But we will just declare this officially.#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<string.h>#include<ctype.h>#include"num.h"struct num {size_t size;/* size of the digits array */char *digits;/* array of digits; each stores numerical value 0..9 in least significant first order */};/* how many digits to leave room for by default */#define NUM_INITIAL_SIZE (4)/* base for numbers */#define NUM_BASE (10)/* create a new empty num */static Num *numCreateZero(void){ Num *n = malloc(sizeof(struct num)); assert(n); n->size = NUM_INITIAL_SIZE; n->digits = calloc(NUM_INITIAL_SIZE,1); assert(n->digits);return n;}/* set the i-th digit of n to d */staticvoidnumSetDigit(Num *n,int i,int d){ assert(0 <= d); assert(d < NUM_BASE);if(i <0) {return; }elseif(i ==0 && i > n->size) {/* don't bother expanding to write a zero that is already implicit */return; }else {/* make sure we have enough room */while(i >= n->size) {size_t newSize =2*n->size; n->digits = realloc(n->digits, newSize);for(size_t j = n->size; j < newSize; j++) { n->digits[j] =0; } n->size = newSize; } n->digits[i] = d; }}Num *numCreate(constchar *s){ Num *n = numCreateZero();size_t len = strlen(s);for(int i =0; i < len; i++) {int d = s[len - i -1];if(!isdigit(d)) {/* error! */ numDestroy(n);return0; }else { numSetDigit(n, i, d -'0'); } }return n;}voidnumDestroy(Num *n){ free(n->digits); free(n);}intnumGetDigit(const Num *n,int i){if(i <0 || i >= n->size) {return0; }else {return n->digits[i]; }}Num *numAdd(const Num *x,const Num *y){ Num *sum = numCreateZero();int carry =0;int s;/* run until we run out of digits on one number or another */for(int i =0; i < x->size || i < y->size || carry !=0; i++) { s = carry + numGetDigit(x, i) + numGetDigit(y, i); numSetDigit(sum, i, s % NUM_BASE); carry = s / NUM_BASE; }return sum;}/* returns x * d * 10**shift */static Num *numPartialProduct(const Num *x,int d,int shift){ Num *result = numCreateZero();int carry =0;int p;for(int i =0; i < x->size || carry !=0; i++) { p = d * numGetDigit(x, i) + carry; numSetDigit(result, i + shift, p % NUM_BASE); carry = p / NUM_BASE; }return result;}Num *numMultiply(const Num *x,const Num *y){ Num *accumulator = numCreateZero(); Num *p;/* partial product */ Num *sum;for(int i =0; i < x->size; i++) { p = numPartialProduct(y, x->digits[i], i); sum = numAdd(accumulator, p); numDestroy(p); numDestroy(accumulator); accumulator = sum; }return accumulator;}voidnumPrint(const Num *n,FILE *f){int i;/* ignore leading zeros */for(i = n->size -1; i >0 && n->digits[i] ==0; i--);/* print remaining digits */for(; i >=0; i--) { putc('0' + n->digits[i], f); }}For this assignment, you are to implement a data type representing a deck of playing cards.
The filedeck.h, also shown in theInterface section below, defines the interface to the data type. Your job is to provide a matchingdeck.c file.
A deck consists of an ordered sequence of 0 or more cards, implemented as thestruct card type. Each card has a rank, which is a character in the string"A23456789TJQK", and a suit, which is a character in the string"CDHS". A card is printed by giving the rank and then the suit. For example, the Ten of Diamonds has rank'T' and suit'D', and would be printed asTD.
A deck is printed by printing all the cards in the deck, separated by spaces. ThedeckPrint function should do this. There should not be a space after the last card.
ThedeckCreate anddeckDestroy functions create and destroy decks. A new deck always contains 52 cards, ordered by suit, then rank.
ThedeckGetCard function removes and returns the card at the top of a deck. ThedeckPutCard function adds a new card to the bottom of a deck.
Two additional functions split and combine decks. ThedeckSplit function takes a deckd, and a numbern, and returns (using pointers passed in by the caller) two decksd1 andd2, whered1 contains the topn cards ind (or all cards ind ifn is greater than or equal to the size ofd), andd2 contains any cards that are left over. As a side effect,deckSplit destroysd. ThedeckShuffle function combines two decksd1 andd2 by alternately taking cards from the top of each deck, starting withd1; if one of the decks runs out, the remaining deck supplies the rest of the cards. LikedeckShuffle,deckSplit returns a new deck and destroys its inputs.
A test harness that you can use to try out your code can be found intestDeck.c. This implementsDeck Assembly Language, a minimalist programming language for manipulating decks. Here is an example of runningtestDeck by hand. Note that inputs and outputs are interleaved.
$ ./testDeck# create a new deck and print itc1 p1AC 2C 3C 4C 5C 6C 7C 8C 9C TC JC QC KC AD 2D 3D 4D 5D 6D 7D 8D 9D TD JD QD KD AH 2H 3H 4H 5H 6H 7H 8H 9H TH JH QH KH AS 2S 3S 4S 5S 6S 7S 8S 9S TS JS QS KS# remove top card from the deck-1 p1AC2C 3C 4C 5C 6C 7C 8C 9C TC JC QC KC AD 2D 3D 4D 5D 6D 7D 8D 9D TD JD QD KD AH 2H 3H 4H 5H 6H 7H 8H 9H TH JH QH KH AS 2S 3S 4S 5S 6S 7S 8S 9S TS JS QS KS# put it back on the bottom+1 AC p12C 3C 4C 5C 6C 7C 8C 9C TC JC QC KC AD 2D 3D 4D 5D 6D 7D 8D 9D TD JD QD KD AH 2H 3H 4H 5H 6H 7H 8H 9H TH JH QH KH AS 2S 3S 4S 5S 6S 7S 8S 9S TS JS QS KS AC# split into two decks/ 1 2 17 p1 p22C 3C 4C 5C 6C 7C 8C 9C TC JC QC KC AD 2D 3D 4D 5D6D 7D 8D 9D TD JD QD KD AH 2H 3H 4H 5H 6H 7H 8H 9H TH JH QH KH AS 2S 3S 4S 5S 6S 7S 8S 9S TS JS QS KS AC# shuffle them back together* 1 2 p12C 6D 3C 7D 4C 8D 5C 9D 6C TD 7C JD 8C QD 9C KD TC AH JC 2H QC 3H KC 4H AD 5H 2D 6H 3D 7H 4D 8H 5D 9H TH JH QH KH AS 2S 3S 4S 5S 6S 7S 8S 9S TS JS QS KS AC# split into a nonempty deck and an empty deck/ 1 2 100000 e1 p1 e2 p212C 6D 3C 7D 4C 8D 5C 9D 6C TD 7C JD 8C QD 9C KD TC AH JC 2H QC 3H KC 4H AD 5H 2D 6H 3D 7H 4D 8H 5D 9H TH JH QH KH AS 2S 3S 4S 5S 6S 7S 8S 9S TS JS QS KS AC0# clean up and exitd1 d2 q// Abstract data type for a deck of playing cards#include<stdio.h>// Standard suits and ranks#define SUITS "CDHS"#define RANKS "A23456789TJQK"// A single card// This is small enough that we usually pass it// around by copying instead of using pointers.typedefstruct card {char rank;/* from RANKS */char suit;/* from SUITS */} Card;// A deck of cardstypedefstruct deck Deck;// Create a new unshuffled deck of 52 cards,// ordered by suit then rank://// AC 2C 3C ... KC AD 2D 3D ... KD AH 2H 3H ... KSDeck *deckCreate(void);// Free all space used by d.// Running time should be O(length of deck)void deckDestroy(Deck *d);// Return true if deck is not empty.// Running time should be O(1).int deckNotEmpty(const Deck *d);// Remove and return the top card of a deck.// If deck is empty, behavior is undefined.// Running time should be O(1).Card deckGetCard(Deck *d);// Add a card to the bottom of a deck.// This is not required to do anything special if the card is bogus// (e.g. "1C", "?X", "A-").// Running time should be O(1).void deckPutCard(Deck *d, Card c);// Split a deck into two piles:// *d1 is new deck with top n cards in d.// *d2 is new deck with remaining cards in d.// Order of cards is preserved.// If d contains fewer than n cards, put them all in d1.// Destroys d.// Running time should be O(n).void deckSplit(Deck *d,int n, Deck **d1, Deck **d2);// Shuffle two decks together by alternating cards from// d1 and d2 to obtain new deck.//// If d1 is X X X X// and d2 is Y Y Y Y Y Y Y,// return value is X Y X Y X Y X Y Y Y Y.//// If d1 is X X X X// and d2 is Y Y,// return value is X Y X Y X X.//// Running time should be O(length of shorter deck).// Destroys d1 and d2.//// If d1 == d2, behavior of this function is undefined.Deck *deckShuffle(Deck *d1, Deck *d2);// Print the contents of deck to f as sequence of ranks/suits// separated by spaces.// Example output: "AS TC 9D 3H 5S" (without quotes)// Running time should be O(length of deck).void deckPrint(const Deck *d,FILE *f);Submit your assignment as usual with/c/cs223/bin/submit 4 deck.c. You can use/c/cs223/bin/testit 4 public to run the public test script on your submitted assignment, which is also available in/c/cs223/Hwk4/test.public.
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include"deck.h"// decks are queues// Invariant:// d->head point to first element or is 0 if deck is empty.// d->tail points to last element if deck is not empty.struct elt {struct elt *next; Card card;};struct deck {struct elt *head;struct elt *tail;};// make an empty deckstatic Deck *deckCreateEmpty(void){ Deck *d; d = malloc(sizeof(Deck)); assert(d); d->head = d->tail =0;return d;}voiddeckPutCard(Deck *d, Card c){struct elt *e; e = malloc(sizeof(struct elt)); assert(e); e->next =0; e->card = c;if(d->head) { d->tail->next = e; }else { d->head = e; }// new tail is always e d->tail = e;}CarddeckGetCard(Deck *d){struct elt *head; Card c; head = d->head;/* blame the called if the deck is empty */ assert(head);/* save card and unlink */ c = head->card; d->head = head->next; free(head);return c;}Deck *deckCreate(void){ Deck *d; Card c; d = deckCreateEmpty();for(int s =0; SUITS[s]; s++) { c.suit = SUITS[s];for(int r =0; RANKS[r]; r++) { c.rank = RANKS[r]; deckPutCard(d, c); } }return d;}intdeckNotEmpty(const Deck *d){return d->head !=0;}voiddeckDestroy(Deck *d){while(deckNotEmpty(d)) { deckGetCard(d); } free(d);}voiddeckSplit(Deck *d,int n, Deck **d1, Deck **d2){// *d1 starts as a new empty deck *d1 = deckCreateEmpty();// *d2 is just going to be d *d2 = d;for(int i =0; i < n && deckNotEmpty(d); i++) { deckPutCard(*d1, deckGetCard(d)); }}// add d1 onto end of d// destroys d1// this is an O(1) operationstaticvoiddeckPutDeck(Deck *d, Deck *d1){if(deckNotEmpty(d1)) {if(d->head) {// paste onto tail d->tail->next = d1->head; }else {// copy into head d->head = d1->head; }// new tail is d1->tail in either case d->tail = d1->tail; } free(d1);}Deck *deckShuffle(Deck *d1, Deck *d2){ Deck *d = deckCreateEmpty(); assert(d1 != d2);while(deckNotEmpty(d1) && deckNotEmpty(d2)) { deckPutCard(d, deckGetCard(d1)); deckPutCard(d, deckGetCard(d2)); }// paste nonempty deck onto end of d1 deckPutDeck(d, d1); deckPutDeck(d, d2);return d;}voiddeckPrint(const Deck *d,FILE *f){for(struct elt *e = d->head; e; e = e->next) { putc(e->card.rank, f); putc(e->card.suit, f);if(e->next) { putc(' ', f); } }}For this assignment, you get to simulate a 3-dimensional universe full of ants.
You are to implement a programants, which takes fromstdin a sequence of commands to an army of 256 ants, each labeled permanently by one of the 256 possible values of achar. The ants move around in a232 × 232 × 232 3-dimensional universe with coordinates given byints. Initially the universe is entirely filled with space characters (' ' == 0x20), but over time the ants may overwrite these with other values. Every ant starts at position(0, 0, 0). It is permitted for multiple ants to be in the same position.
At any time, there is exactly one ant receiving commands, which is referred to as thecurrent ant.
This ant receives a sequence of commands fromstdin, each consisting of a single character, the effects of which are described in the list of commands below.
The initial current ant is given by whatever character appears first onstdin.
Subsequently, which ant is the current ant may be set by the\n command.
hjkl*.?stdout.\ncharacter\n is immediately followed by end-of-file, ignore it and exit.Any other characters are ignored, which allows for comments written in all caps that avoid certain punctuation characters.
An ant that attempts to walk off an edge of the plane or jump to a coordinate outside the range of anint wraps around using the usual Cint overflow rules (in other words, you don’t need to test for overflow).
#?makes ant# print a space.
#lll.-lll?l?h?makes ant# move 3 positions east and write#. Ant- then moves 3 positions east, prints the#, moves another position east, prints a space, moves a position west, and prints the# again.
h.?e.?lj.??oh.?,.?k?w.?o?r.?l?d.?.?printshello, world followed by a newline. (Note the use of “newline ant” in the last command.)
#j.*.*.*.k.=j?j?j?j?j?j?j?j?j?j?moves ant# south 7 positions using a combinations of moves and doublings, then sends ant= after it to show all the places ant# landed.
#.hj<lk>?moves ant# around a 3-dimensional path that ends back where it started.
Using/c/cs223/bin/submit 5, submit all source and header files needed to build your program, along with aMakefile that will generate a programants whenmake is run with no arguments.
Sample inputs and corresponding outputs can be found in/c/cs223/Hwk5/testFiles. The public test script is in/c/cs223/Hwk5/test.public. It can be run on your submitted files with/c/cs223/bin/testit 5 public.
The solution is spread across several files:
pointMove function for applying an operator to a point.Ants abstraction fromants.c.ants program from these files.For this assignment, you are to read in a description of an ordered tree fromstdin, reorder the children of each node by descreasing order of the size of their subtrees, and write the result tostdout.
Atree in this context consists of aroot node and 0 or more subtrees. Each subtree also consists of a root node and 0 or more subtrees. The subtrees below each node are ordered. This means that the trees crudely depicted below are not identical, even though they have the same subtrees in a different order:
* * /|\ /|\ * * * * * * | |\ /| | * * * * * *Thesize of a subtree is the number of nodes in the subtree. The smallest possible subtree consists of a single root node with no children and has size 1. Both of the trees in the picture above have size 7, with the three subtrees of the root having sizes 1, 2, and 3 in the tree on the left and 3, 2, and 1 in the tree on the right.
When sorting by decreasing size, you should reorder the subtrees below each node so that the subtree with largest size comes first, followed by next largest, and so on until you reach the smallest subtree. You may break ties between subtrees of equal size arbitrarily.
For the input and output to your program, a tree will be represented recursively as a pair of square brackets with all subtrees of the root represented in order between them. Some examples of representations and the corresponding trees are given below:
[] [[]] [[][]] [[][[]]] [[][[]][[][]]]* * * * * | / \ / \ /|\ * * * * * * * * | | |\ * * * *Your program should read fromstdin until it obtains the complete representation of a tree. You do not need to check for extra characters that come after the tree (so[]! is not an error), but if you get an incomplete tree ([), mismatched brackets (]), or extraneous characters that are not brackets ([cow]), your program should produce no output and exit with a nonzero exit code.
The output should match the format of the input. You should not write anything other than the representation of the tree in the non-error case.
Here are the sorted versions of the trees in the picture from the previous section:
[] [[]] [[][]] [[[]][]] [[[][]][[]][]]* * * * * | / \ / \ /|\ * * * * * * * * | /| | * * * *Note that actual input and outputs would be just the individual bracket representations. Whitespace and the ASCII art pictures are included only to make it easier to see what the trees looks like.
You may assume that any tree in the input is shallow enough that operating on its subtrees recursively in a sensible way will not overflow the system stack.
Using/c/cs223/bin/submit 6, submit all source and header files needed to build your program, along with aMakefile that will generate a programtreeSort whenmake is run with no arguments.
Sample inputs and corresponding outputs can be found in/c/cs223/Hwk6/testFiles. The public test script is in/c/cs223/Hwk6/test.public. It can be run on your submitted files with/c/cs223/bin/testit 6 public.
For this assignment, you are to implement an abstract data type representing an array that supports an efficientarrayCombine operation that returns the result of aggregating the firstk elements of the array using acombine function provided when the array is first created.
You may assume thatcombine is an associative operation:combine(x, combine(y, z)) should always equalcombine(combine(x, y), z). But it is not necessarily commutative:combine(x, y) may or may not equalcombine(y, x).
The interface to the array is given by the filearray.h, shown below:
#ifndef _ARRAY_H#define _ARRAY_H// Abstract array data type that allows quick computation of an// aggregate over a prefix of the array.//// The array is provided with a combine function for combining// values. This should represent some associative binary operation on ints.//// The arrayCombine function returns the result of aggregating// the first k values in the array in order using combine.#include<stdlib.h>typedefstruct array Array;// Create a new array holding n values, all initially 0.// Behavior is undefined if n == 0.// Cost: O(n).Array *arrayCreate(int (*combine)(int,int),size_t n);// Free all space used by an array.// Cost: O(n).void arrayDestroy(Array *);// Return the number of elements of an array.// Cost: O(1).size_t arraySize(const Array *);// Return the i-th element of an array// or 0 if i is out of range.// Cost: O(log n).int arrayGet(const Array *,size_t i);// Set the i-th element of an array to v.// No effect if i is out of range.// Cost: O(log n).void arraySet(Array *,size_t i,int v);// Return the result of aggregating the first k elements// of an array in order using the combine function.// If k is zero or greater than size, returns combination of all elements.// Cost: O(log n).int arrayCombine(const Array *,size_t k);#endifThis file can also be found in/c/cs223/Hwk7/array.h on the Zoo.
Your task is to write a correspondingarray.c file that implements the data structure and functions declared inarray.h. You should stay within the stated asymptotic bounds for each function.
Most of the functions inarray.h are self-explanatory. The tricky one isarrayCombine. For values ofk in the range 1 through the size of the array, thearrayCombine routine should produce the same result as running the following function, wherecombine is the function previously passed toarrayCreate.
intarrayCombineIterative(const Array *self,size_t k,int (*combine)(int,int)){int accumulator = arrayGet(self,0);for(int i =1; i < k; i++) { accumulator = combine(accumulator, arrayGet(self, i)); }return accumulator;}Note that this implementation runs inO(n) time, which is too much. Your implementation should ensure thatarrayCombine runs inO(log n) time. You may find it helpful to store aggregate data computing the result of combining specific intervals of the array, to allow you to compute the aggregate for a given prefix by combining the values of some of these intervals. But be careful not to store aggregates for too many intervals, or you will spend too much time updating this information inarraySet.
The test harness used by the grading script can be found in/c/cs223/Hwk7/testArray.c. This provides commands for creating arrays with various combining functions, running individual functions fromarray.h on these arrays, and running several stress tests designed to measure the efficiency of an array implementation as a function of its size. You should feel free to adapt this program as needed to test your own implementation. The test script will always use the official version.
Sample test inputs and outputs can be found in/c/cs223/Hwk7/testFiles/.
Submit your program with/c/cs223/bin/submit 7 array.c. You do not need to submit any other files besides this one. Any other files will be ignored by the grading script.
There are many ways to do this, but getting the desired efficiency onarrayCombine pretty much requires keeping track of aggregate data in some sort of tree structure. The following file does this with an implicit tree packed into arrays. This particular structure is known as asegment tree, and was invented by John Bentley in 1977. It also works to build an explicit tree out of pointers.
#include<stdlib.h>#include<assert.h>#include<limits.h>// for CHAR_BIT#include"array.h"#define LEVELS (sizeof(size_t) * CHAR_BIT)struct array {int (*combine)(int,int);size_t n;int *data[LEVELS];// data[level][index] points to element};#ifdef SANITY_CHECK// turning on SANITY_CHECK enables (expensive!)// sanity-checking in fixEntry.// return result of combining interval [k1,k2)staticintarrayCombineIterative(const Array *self,size_t k1,size_t k2){ assert(k1 < k2); assert(k2 <= self->n);int accumulator = self->data[0][k1];for(size_t i = k1+1; i < k2; i++) { accumulator = self->combine(accumulator, self->data[0][i]); }return accumulator;}#endif// fix entry in self->data[level][i]staticvoidfixEntry(Array *self,int level,size_t i){ assert(level >0); assert(level < LEVELS); assert(i < (self->n >> level)); self->data[level][i] = self->combine( self->data[level-1][2*i], self->data[level-1][2*i+1]);#ifdef SANITY_CHECK assert(self->data[level][i] == arrayCombineIterative(self, i << level, (i+1) << level));#endif}Array *arrayCreate(int (*combine)(int,int),size_t n){ assert(n >0);struct array *self = malloc(sizeof(struct array)); self->combine = combine; self->n = n;// level i is combined groups of 2^i elements// and has size n >> ifor(int level =0; (n >> level) >0; level++) { self->data[level] = calloc(n >> level,sizeof(int)); }// bottom layer is already 0.// combine elements for higher levelsfor(int level =1; (n >> level) >0; level++) {for(size_t i =0; i < (n >> level); i++) { fixEntry(self, level, i); } }return self;}size_tarraySize(const Array *self){return self->n;}intarrayGet(const Array *self,size_t i){if(i < self->n) {return self->data[0][i]; }else {return0; }}voidarraySet(Array *self,size_t i,int v){if(i < self->n) { self->data[0][i] = v;// fix summary datafor(int level =1; (i >> level) < (self->n >> level); level++) { fixEntry(self, level, i >> level); } }}// aggegate the first k elements at given levelstaticintcombineHelper(const Array *self,int level,size_t k){ assert(k >0);if(k ==1) {// return first elementreturn self->data[level][0]; }elseif(k %2 ==0) {// move up a levelreturn combineHelper(self, level+1, k /2); }else {// compute prefix by moving up a level, then add last elementint prefix = combineHelper(self, level+1, k /2);return self->combine(prefix, self->data[level][k-1]); }}intarrayCombine(const Array *self,size_t k){int result;if(k ==0 || k > self->n) { k = self->n; } result = combineHelper(self,0, k);return result;}voidarrayDestroy(Array *self){for(int level =0; (self->n >> level) >0; level++) { free(self->data[level]); } free(self);}For this assignment, you are to implement a solver for a simple solitaire game using cards marked with lowercase letters.
Given a string, imagine that it is a line of cards, arranged from left to right. The rules of the game are:
"aeoiu") or if their ASCII values differ by at most 5.Suppose the initial word isshrink.
Becausei andn are 5 apart in their ASCII encodings and adjacent, we can move then on top of thei to getshrnk.
Becauseh andk are 3 apart in their ASCII encodings and have two cards between them, we can move thek on top of theh to getskrn.
Becauser andn are 4 apart in their ASCII encodings and adjacent, we can move then on top of ther to getskn.
Becausek andn are 3 apart in their ASCII encodings and adjacent, we can move then on top of thek to getsn.
Becauses andn are 5 apart in their ASCII encodings and adjacent, we can move then on top of thes to getn.
Since we ended up with a single letter, we won. (Note that there might be other ways to win in this case.)
We can summarize the play of the game by printing each word, starting withshrink and ending withn, like this:
shrinkshrnkskrnsknsnnBecauseshrink has only one vowel, we couldn’t use the vowel rule. An example of a sequence that uses the vowel rule is this reduction ofroad:
roadoadaddIn the second step, we can movea ontoo even though their ASCII values differ by 14, because they are both vowels.
If we start withax, we can’t win, becausea andx differ by too much, and only one of them is a vowel.
Write a programshrink that takes a word asargv[1] and either prints out a reduction to a single letter following the rules defined above, or prints nothing if there is no such reduction. In either case your program should exit with a zero error code.
Some typical executions ofshrink might look like this:
$ ./shrink yaleuniversityyaleuniversityyaluniversityyaluiversityyaluiversiyyiluversiyyluversiyyluvirsyyluvisyyiuvsyyuvsyuvsyyvsvss$ ./shrink elephant$ ./shrink aaYou should try to pick an approach to solving the problem that is reasonably efficient. If you solve the program using some sort of search, it may be helpful to include a mechanism that prevents searching past the same intermediate result more than once.
It might be polite to check for bad inputs (for example, calling./shrink with no arguments or with a word that contains a character that is not a lowercase ASCII letter), but you are not required to do so, and the test script will supply only good inputs.
Submit all source and header files needed to buildshrink, along with aMakefile, using/c/cs223/bin/submit 8. As usual, you can test your submitted files with/c/cs223/bin/testit 8 public, or test files in your local directory with/c/cs223/Hwk8/test.public.
Below is the main fileshrink.c that does the actual search. This uses an implementation of a data type representing a set of strings, defined inset.h, which can easily be implemented using a hash table as inset.c or a ternary search tree as intstSet.c. Both versions can be built using the sampleMakefile. The hash table version is a bit faster.
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<string.h>#include"set.h"staticintisVowel(int c){switch(c) {case'a':case'e':case'i':case'o':case'u':return1;default:return0; }}#define MAX_CLOSE_DISTANCE (5)staticintisClose(int c1,int c2){return abs(c1-c2) <= MAX_CLOSE_DISTANCE;}staticintisMatch(int c1,int c2){return isClose(c1, c2) || (isVowel(c1) && isVowel(c2));}// used for history linked liststruct history {char *word;conststruct history *prev;};staticvoidprintHistory(conststruct history *current){if(current) { printHistory(current->prev); puts(current->word); }}#define LONG_OFFSET (3)// attempts to shrink prev->word to one character// if it works, prints sequence of intermediate words// and returns 1, else returns 0.staticintshrink(Set *found,conststruct history *prev){size_t n = strlen(prev->word);if(n <=1) {// we are done printHistory(prev);return1; }elseif(setContains(found, prev->word)) {// dead endreturn0; }else {// mark as previously found setInsert(found, prev->word);// try all reductionsstruct history current; current.prev = prev; current.word = malloc(n);// start with the short-offset reductionsfor(size_t i =0; i+1 < n; i++) {if(isMatch(prev->word[i], prev->word[i+1])) {// shrink and recurse strncpy(current.word, prev->word, i); strcpy(current.word + i, prev->word + i +1);if(shrink(found, ¤t)) {// got it free(current.word);return1; } } }// now the long-offset onesfor(size_t i =0; i + LONG_OFFSET < n; i++) {if(isMatch(prev->word[i], prev->word[i+LONG_OFFSET])) {// shrink and recurse strncpy(current.word, prev->word, i); current.word[i] = prev->word[i+LONG_OFFSET]; strncpy(current.word + i +1, prev->word + i +1, LONG_OFFSET -1); strcpy(current.word + i + LONG_OFFSET, prev->word + i + LONG_OFFSET +1);if(shrink(found, ¤t)) {// got it free(current.word);return1; } } }// found nothing free(current.word);return0; }}intmain(int argc,char **argv){if(argc !=2) { fprintf(stderr,"Usage: %s word\n", argv[0]);return1; }struct history current; current.prev =0; current.word = argv[1]; Set *s = setCreate(); shrink(s, ¤t); setDestroy(s);return0;}Make sure that you sign up for an account on the Zoo athttp://zoo.cs.yale.edu/accounts.html. If you already have an account, you still need to check the CPSC 223 box so that you can turn in assignments. It’s best to do this as soon as possible.
You do not need to develop your solution on the Zoo, but you will need to turn it in there, and it will be tested using the compiler on the Zoo.
For this assignment, you are to implement an encoder for a polyalphabetic substitution cipher vaguely inspired by the Enigma machines used by Germany during World War 2. Unlike the Enigma machine, this cipher doesn’t provide much security, so you can probably tell your non-US-national friends about it without violating US export control laws.27
Each letter'A' through'Z' or'a' through'z' is encrypted by shifting it some number of positions forward in the alphabet, wrapping around at the end.
The number of positions is determined by an offset that changes over time. The initial shift is 17 positions. After encoding an uppercase letter, the shift is increased by 5. After encoding a lowercase letter, the shift is increased by 3. To avoid overflow on long texts, it’s probably a good idea to store the offset modulo 26.
An uppercase letter is always encoded by an uppercase letter, and a lowercase letter is always encoded by a lowercase letter. All other characters are passed through intact.
Below is an example of encoding a famous Napoleonic palindrome using this cipher:
Plaintext: A b l e w a s I e r e I s a w E l b aOffset: 17 22 25 2 5 8 11 14 19 22 25 2 7 10 13 16 21 24 1Ciphertext: R x k g b i d W x n d K z k j U g z bFor this assignment, you are to write a programencode.c that takes a plaintext fromstdin, encodes it using the above algorithm, and writes the result tostdout.
For example, given the input
"Stop, thief!" cried Tom, arrestingly."You'll never take me alive!" replied the criminal, funereally.Your program should output
"Jpnr, yptsw!" woihj Ccd, uorhycucygw."Zud'xa fztfv akxu fa znndp!" fvjiihj ctt umgnmuky, vnjdtjiwzp.'A' through'Z' and'a' through'z' are represented using continuous ranges of integers, so that the expressionc - 'A' gives the position ofc in the alphabet, providedc is an uppercase character, and countingA as0. This means that your program will not be portable to machines that use EBCDIC or some other exotic character representation.#include <ctype.h> in your program and use theisupper andislower macros. Note that these may behave oddly if you have set a locale that uses a different alphabet. It may be safer to make your own tests.Sample inputs and outputs can be found in/c/cs223/Hwk1/testFiles on the Zoo. Note that some of these files contain non-printing characters that may have odd effects if you send them to your screen. The safest way to test if your program produces the same output as the sample output is probably to usecmp, for example:
$ ./encode < test.in > tmp$ cmp tmp test.outIftmp andtest.out contain the same characters,cmp will say nothing. Otherwise it will tell you the first position where they differ.
If you want to see what characters are in a binary file, trying usingod -t x1z, as in
$ echo hi > file$ cat filehi$ od -t x1z file0000000 68 69 0a >hi.<0000003Submit your assignment using the command:
/c/cs223/bin/submit 1 encode.cYou can test that your program compiles (and passes a few basic tests) using the command:
/c/cs223/bin/testit 1 encodeThis runs the test script in/c/cs223/Hwk1/test.encode on your submitted assignment. You can also run this script by hand to test the version ofencode.c in your current working directory.
The unsympathetic robo-grading script used to grade this assignment may or may not use the same tests as this command, so you should make sure your program works on other inputs as well. You may also want to look at thestyle grading checklist to see that you haven’t committed any gross atrocities against readability, in case a human being should happen to look at your code.
You can submit your assignment more than once, but any late penalties will be assessed based on the last submission. For more details about thesubmit script and its capabilities, seehere.
/* * Encode text on stdin by alphabet rotation with shifting offset. * * Initially, each character 'A'..'Z' or 'a'..'z' is rotated 17 positions. * * After encoding an uppercase letter, the offset is increased by 5 (mod 26). * * After encoding a lowercase letter, the offset is increased by 3 (mod 26). * * These parameters are set using the INITIAL_OFFSET, UPPERCASE_STEP, and LOWERCASE_STEP * constants defined below. * */#include<stdio.h>#define INITIAL_OFFSET (17)#define UPPERCASE_STEP (5)#define LOWERCASE_STEP (3)#define MODULUS ('z' - 'a' + 1)intmain(int argc,char **argv){int offset = INITIAL_OFFSET;int c;while((c = getchar()) != EOF) {if(('a' <= c) && (c <='z')) { putchar(((c -'a') + offset) % MODULUS +'a'); offset = (offset + LOWERCASE_STEP) % MODULUS; }elseif(('A' <= c) && (c <='Z')) { putchar(((c -'A') + offset) % MODULUS +'A'); offset = (offset + UPPERCASE_STEP) % MODULUS; }else { putchar(c); } }return0;}The Hollywood Hackable Safe Company has announced a new line of electronically-controlled safes with a C API to permit maximum opening speed while still protecting your valuable loot. This interface is defined in the following file, which can also be found on the Zoo in the directory/c/cs223/Hwk2/sourceFiles:
/* * API for safes made by the * Hollywood Hackable Safe Company LLC. */typedefstruct safe Safe;/* opaque data type for a safe *//* * Returns the number of tumblers on a safe. * If this is n, the possible tumbler indices will be 0 through n-1. * */int numTumblers(Safe *s);/* * Returns the number of positions of each tumbler. * If this is n, the possible tumbler positions will be 0 through n-1. */int numPositions(Safe *s);/* Return codes for tryCombination */#define SAFE_BAD_COMBINATION (-1)#define SAFE_SELF_DESTRUCTED (-2)/* * Try a combination. * * This should be an array of numTumbler(s) ints. * * Returns contents of safe (a non-negative int) if combination is correct * and safe has not yet self-destructed. * * Returns SAFE_BAD_COMBINATION if combination is incorrect * and safe has not yet self-destructed. * * Returns SAFE_SELF_DESTRUCTED if safe has self-destructed. * * Note: may modify combination. */int tryCombination(Safe *s,int *combination);The noteworthy function in this API istryCombination, which takes a pointer to a safe and an array ofints representing the combination, and returns either the contents of the safe (anint), the special codeSAFE_BAD_COMBINATION if the combination is incorrect, or the special codeSAFE_SELF_DESTRUCTED if the safe blew up after seeing too many bad combinations. Note thattryCombination does not declare its second argument to beconst and may not leave it intact. The additional functions allow you to obtain important information about the safe, like how many tumblers it has and what values these tumblers can be set to. The behavior of a safe given a combination with the wrong number of values or values outside the permitted range is undefined.
Your task is to write a functionopenSafe that will open a safe, if possible, by trying all possible combinations. Note that if the safe self-destructs before you can try all the possibilities, this task may not in fact be possible. YouropenSafe function should returnSAFE_SELF_DESTRUCTED in this case. Your function should be defined in a fileopenSafe.c and should match the declaration in this file:
/* Include safe.h before this file to get the definition of Safe. *//* * Open a safe and return the value returned by tryCombination, * or SAFE_SELF_DESTRUCTED if the safe self-destructed. */int openSafe(Safe *s);It is recommended that you put the lines below in youropenSafe.c file to ensure consistency with these declarations:
You may put additional functions inopenSafe.c if that would be helpful. You should declare thesestatic to avoid the possibility of namespace conflicts.
In addition tosafe.h andopenSafe.h,/c/cs223/Hwk2/sourceFiles also contains amain.c file that can be compiled together withopenSafe.c to generate a program that can be called from the command line. This program generates a safe with a pseudorandom combination based on parameters specified on the command line, runs youropenSafe routine on it, and prints the value thatopenSafe returns. You should not rely on your function being tested with this particular program.
Submit your assignment as usual with
/c/cs223/bin/submit 2 openSafe.cYou do not need to submit any other files (and the test script will ignore them if you do).
You can test that your program compiles and passes a few basic tests with the command
/c/cs223/bin/testit 2 openSafeThis runs the test script in/c/cs223/Hwk2/test.openSafe on your submitted assignment. You can also run this script by hand to test the version ofopenSafe.c in your current working directory.
You may need to allocate storage usingmalloc to complete this assignment. If you do so, you should make sure that you callfree on any block you allocate inside youropenSafe function before the function returns. Thetest.openSafe script attempts to detect storage leaks or other problems resulting from misuse of these routines by running your program withvalgrind. You can also usevalgrind yourself to track down the source of errors, particularly if you remember to compile with debugging info turned on using the-g3 option togcc. The script/c/cs223/bin/vg gives a shortcut for runningvalgrind with some of the more useful options.
#include<stdlib.h>#include<assert.h>#include"safe.h"#include"openSafe.h"/* set combination to all zeros */staticvoidzeroCombination(int n,int *combination){int i;for(i =0; i < n; i++) { combination[i] =0; }}/* non-destructive version of tryCombination */staticintnondestructiveTryCombination(Safe *s,constint *combination){int *copy;/* duplicate of combination */int result;/* result of tryCombination */int n;/* number of tumblers */int i; n = numTumblers(s); copy = (int *) malloc(sizeof(int) * n);for(i =0; i < n; i++) { copy[i] = combination[i]; } result = tryCombination(s, copy); free(copy);return result;}/* update combination to next value */staticvoidnextCombination(int n,int base,int *combination){int i;/* we are essentially incrementing an n-digit number in given base *//* this means setting any digit that overflows to 0 and continuing *//* until we get a digit we can increment without carrying */for(i =0; i < n && ++(combination[i]) >= base; i++) { combination[i] =0; }}intopenSafe(Safe *s){int *combination;/* counter for combinations */int n;/* number of tumblers */int base;/* number of positions */int result;/* result of tryCombination *//* allocate space */ n = numTumblers(s); base = numPositions(s); combination = malloc(sizeof(int) * n); assert(combination);for(zeroCombination(n, combination); (result = nondestructiveTryCombination(s, combination)) == SAFE_BAD_COMBINATION; nextCombination(n, base, combination)); free(combination);return result;}Given a strings, anquadratic letter sequence fors is defined by giving non-negative integer coefficientsc0, c1, c2, where at least one ofc1 andc2 is not zero, and computing the sequence of letterss[c0 + c1 ⋅ i + c2 ⋅ i2] fori = 0, 1, 2, ….
These can be used to hide secret message inside larger texts. For example, the famous Napoleonic palindrome “Able was I ere I saw Elba” hides the word “bIb” at positions 1, 9, and 23, which are generated byc0 = 1,c1 = 5 andc2 = 3:
| i | c0 + c1i + c2i2 |
|---|---|
| 0 | 1 = 1 + 5 ⋅ 0 + 3 ⋅ 0 |
| 1 | 9 = 1 + 5 ⋅ 1 + 3 ⋅ 12 |
| 2 | 23 = 1 + 5 ⋅ 2 + 3 ⋅ 22 |
Similarly, we can use quadratic letter sequences to reveal secret messages hidden in thelyrics of K-pop songs:
$ ./qls hail satan < gangnam-style-excerpt.txt470 3 5 hail14 10 30 satan14 56 7 satanor even examineAct 1 ofThe Tempest to help resolve theShakespeare authorship question:28
$ ./qls "Bacon" "de Vere" "Marlowe" "Stanley" "that Stratford dude" < tempest-act-one.txt120 387 777 Bacon120 542 906 Bacon120 851 850 Bacon120 1592 726 Bacon120 1607 472 Bacon120 2461 95 Bacon120 2729 50 Bacon120 3225 215 Bacon120 3420 284 Bacon120 4223 330 Bacon120 4534 76 Bacon120 5803 29 Bacon143 46 161 Bacon143 268 727 Bacon143 684 1434 Bacon[... 280 more lines of Bacon omitted ...]19959 1178 87 Bacon5949 239 465 MarloweWrite a programqls.c that takes a text onstdin and searches for quadratic letter sequences that start with the strings given inargv. Your program should output all such quadratic letter sequences that it finds, using the format
where[...] should be replaced by appropriate expressions to givec0,c1,c2, and the string found.
If a string appears more than once at the start of a quadratic letter sequence, your program should print all occurrences. The order your output lines appear in is not important, since the test script sorts them into a canonical order. Do whatever is convenient.
Your program should be reasonably efficient, but you do not need to get carried away looking for a sophisticated algorithm for this problem. Simply testing all plausible combinations of coefficients should be enough.
Because neither K-pop songs nor Elizabethan plays use null characters, you may assume that no null characters appear in your input.
You may also assume that any search strings will contain at least two characters, in order to keep the number of outputs finite.
Submit your assignment as usual with
/c/cs223/bin/submit 3 qls.cYou can run some basic tests on your submitted solution with
/c/cs223/bin/testit 3 qlsThe test program is also available as/c/cs223/Hwk3/test.qls. Sample inputs and outputs can be found in/c/cs223/Hwk3/testFiles. The title of each file contains the test strings used, separated by- characters. Before comparing the output of your program to the output files, you may find it helpful to run it throughsort, e.g.
./qls hail satan < hail-satan.in | sort > test.outdiff test.out hail-satan.out/* * Search for quadratic letter sequences starting with words from argv on stdin. * * A quadratic letter sequence of length n in s is a sequence of characters * * s[c0 + c1*i + c2*i*i] * * where c0, c1, c2 are all >= 0, at least one of c1 and c2 is > 0, * and i ranges over 0, 1, 2, ..., n-1. * * For each QLS found, prints c0, c1, c2, and the target string to stdout. */#include<stdio.h>#include<stdlib.h>#include<string.h>#include<assert.h>#define NUM_COEFFICIENTS (3)/* how many coefficients to pass around *//* * Return true iff we get a match in s for t with coefficients c * * Behavior is undefined if coefficients would send us off the end of s. */staticintqlsMatch(constchar *s,constchar *t,int c[NUM_COEFFICIENTS]){int i;for(i =0; t[i] != '\0'; i++) {if(s[c[0] + c[1] * i + c[2] * i * i] != t[i]) {/* no match */return0; } }return1;}/* * Search for quadratic letter sequences in s starting with t * and print results to stdout. */staticvoidqlsSearch(constchar *s,constchar *t){int c[NUM_COEFFICIENTS];/* coefficients */int lenS;/* length of s */int lenT;/* length of t */int maxI;/* maximum value for i (this is lenT-1) */ lenS = strlen(s); lenT = strlen(t); maxI = lenT-1;/* try all possible c[0] that will let us finish before lenS */for(c[0] =0; c[0] + maxI < lenS; c[0]++) {/* if s[c[0]] isn't right, c[1] and c[2] can't fix it */if(s[c[0]] == t[0]) {/* try all feasible c[1] */for(c[1] =0; c[0] + c[1] * maxI < lenS; c[1]++) {/* try all feasible c[2], but start at 1 if c[1] == 0 */for(c[2] = (c[1] ==0); c[0] + c[1] * maxI + c[2] * maxI * maxI < lenS; c[2]++) {/* now see if we get a match */if(qlsMatch(s, t, c)) { printf("%d %d %d %s\n", c[0], c[1], c[2], t); } } } } }}/* used internally by getContents; initial size of buffer */#define INITIAL_BUFFER_SIZE (16)/* * Return a single string holding all characters from stdin. * * This is malloc'd data that the caller should eventually free. */staticchar *getContents(void){size_t size;size_t len;char *text;int c; size = INITIAL_BUFFER_SIZE; len =0; text = malloc(size); assert(text);while((c = getchar()) != EOF) {/* grow the buffer if full */if(len >= size) { size *=2; text = realloc(text, size); assert(text);} text[len++] = c; }/* cleanup */ text = realloc(text, len+1); assert(text); text[len] = '\0';return text;}intmain(int argc,char **argv){int i;char *s; s = getContents();for(i =1; i < argc; i++) { qlsSearch(s, argv[i]); } free(s);return0;}For this assignment you are to write a program that takes fromstdin a sequence of instructions for pasting ASCII art pictures together, reads those pictures from files, and writes the combined picture tostdout.
Each instruction is of the formrow column filename, suitable for reading withscanf("%d %d %s", &row, &col, filename);, whererow andcol are declared asints andfilename is a suitably large buffer ofchars. Such an instruction means to paste the contents of filefilename into the picture with each character shiftedrow rows down andcolumn columns to the right of its position in filefilename. When pasting an image, all characters other than space (' ', or ASCII code 32) overwrite any characters from earlier files at the same position. Spaces should be treated as transparent, having no effect on the final image.
For example, suppose that the current directory contains these files:
# # #\==========/ \......../examples/2015/hw/4/ship /\ /vv\/vvvv\ ||examples/2015/hw/4/tree * * * ____|_|_|_____ |_____________||___HAPPY_____||__BIRTHDAY___||_____________|examples/2015/hw/4/cakeThen this is what we should get from executing the command:
$ echo "1 1 ship 3 5 ship 3 19 tree 7 2 ship 13 4 ship 4 22 tree 5 6 cake" | ./compositor # # # \==========/ \......#.# # /\ \==========/ /vv\/\ \....*.*.* /vvv/vv\ ____|_|_|_____|/vvvv\ |_____________| || \===|___HAPPY_____| \..|__BIRTHDAY___| |_____________| # # # \==========/ \......../examples/2015/hw/4/example.outFor this assignment, you may submit whatever source files you like, along with a fileMakefile that will generate the programcompositor whenmake is called with no arguments (see theinstructions for using make.)
You can test your submitted assignment using the public test script with
/c/cs223/bin/testit 4 publicYou may also test your unsubmitted assignment in the current working directory with
/c/cs223/Hwk4/test.publicThe test script is intended mostly to guard against trivial errors in output format and is not necessarily exhaustive.
For parsing the commands onstdin, we recommend usingscanf. You can test for end of file by checking ifscanf correctly parsed all three arguments, as in
int row;int col;char filename[BUFFER_SIZE];while(scanf("%d %d %s", &row, &col, filename) ==3) {/* do something with this file */ }You may assume thatrow andcol are always non-negative.
Your program should exit with a non-zero error code if it cannot open a file for reading. Becausescanf’s%s conversion specifier only reads up to the next whitespace character, you may assume that filenames do not contain whitespace. You may also assume that no filename appearing in the input will require more than 2048 bytes to store (including the terminal null character).29
You may assume that the input contains no null characters.
Your output should include newline and space characters to put the composited characters in the appropriate rows and columns. It should not include any more of such characters than are absolutely necessary.
For example, there should never be a space at the end of a line (even if there is a space at the end of a line in one of the input files). Similarly, there should not be any blank lines at the end of your output. You may, however, find it necessary to add a newline to the end of the last line to avoid having the output end in the middle of a line.
You may assume that the final picture is not so big that you can’t store a row or column number for one of its characters in anint.
I wrote two versions of this.The first used a jagged array to represent an image, but I decided I didn’t like it and did another version using a sorted linked list of points. This second version is shown below.
/* * Alternate version of ASCII art thing using a queue. */#include<stdio.h>#include<stdlib.h>#include<assert.h>/* * Idea of this data structure is that we have a sorted array * of pixels, where each pixel specifies a row, column, and character * to put in that position. The sort order is row then column. * * This is organized as a queue in the sense that we can push * new pixels on to the end of it, although as it happens we * never actually dequeue anything. */struct pixel {int row;int col;char value;};struct queue {size_t top;/* number of elements */size_t size;/* number of allocated slots */struct pixel *pixels;/* pixel values, sorted by row then column */};#define QUEUE_INITIAL_SIZE (16)/* create new empty queue */struct queue *queueCreate(void){struct queue *q; q = malloc(sizeof(struct queue)); assert(q); q->top =0; q->size = QUEUE_INITIAL_SIZE; q->pixels = malloc(sizeof(struct pixel) * q->size); assert(q->pixels);return q;}/* clean up queue */voidqueueDestroy(struct queue *q){ free(q->pixels); free(q);}/* add a new pixel to queue */voidqueuePush(struct queue *q,struct pixel p){while(q->top >= q->size) { q->size *=2; q->pixels = realloc(q->pixels,sizeof(struct pixel) * q->size); assert(q->pixels); } q->pixels[q->top++] = p;}/* returns malloc'd data, free with queueDestroy */struct queue *queueRead(constchar *filename){FILE *f;struct queue *q;struct pixel p;int c; q = queueCreate(); f = fopen(filename,"r");if(f ==0) { perror(filename); exit(1); } p.row = p.col =0;while((c = getc(f)) != EOF) {switch(c) {case'\n': p.row++; p.col =0;break;case' ': p.col++;break;default: p.value = c; queuePush(q, p); p.col++;break; } } fclose(f);return q;}/* write pixels in queue to stdout */voidqueueWrite(conststruct queue *q){int outputRow =0;int outputCol =0;int i;for(i =0; i < q->top; i++) {while(outputRow < q->pixels[i].row) { putchar('\n'); outputRow++; outputCol =0; }while(outputCol < q->pixels[i].col) { putchar(' '); outputCol++; } putchar(q->pixels[i].value); outputCol++; }/* end last row */ putchar('\n');}/* * Merge two queues, creating a new, freshly-allocated queue. * New queue is sorted. If there are pixels in both left * and right with the same row and column, the one from right * overwrites the one from left. */struct queue *queueMerge(conststruct queue *left,conststruct queue *right){int l =0;int r =0;struct queue *q; q = queueCreate();while(l < left->top && r < right->top) {if(left->pixels[l].row < right->pixels[r].row) { queuePush(q, left->pixels[l++]); }elseif(left->pixels[l].row == right->pixels[r].row) {if(left->pixels[l].col < right->pixels[r].col) { queuePush(q, left->pixels[l++]); }elseif(left->pixels[l].col == right->pixels[r].col) {/* right wins but both increment */ queuePush(q, right->pixels[r++]); l++; }else {/* right is earlier */ queuePush(q, right->pixels[r++]); } }else {/* right is earlier */ queuePush(q, right->pixels[r++]); } }/* clean out whichever tail is still nonempty */while(l < left->top) { queuePush(q, left->pixels[l++]); }while(r < right->top) { queuePush(q, right->pixels[r++]); }return q;}/* in-place offset by r rows and c columns */voidqueueOffset(struct queue *q,int r,int c){int i;for(i =0; i < q->top; i++) { q->pixels[i].row += r; q->pixels[i].col += c; }}/* max filename size as promised in assignment text */#define BUFFER_SIZE (2048)intmain(int argc,char **argv){struct queue *merged;/* holding place for result of merge */struct queue *left;/* accumulated picture */struct queue *right;/* new picture */int row;/* row offset for new picture */int col;/* column offset for new picture */char filename[BUFFER_SIZE];/* filename for new picture */if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; }for(left = queueCreate(); scanf("%d %d %s", &row, &col, filename) ==3; left = merged) { right = queueRead(filename); queueOffset(right, row, col); merged = queueMerge(left, right); queueDestroy(left); queueDestroy(right); } queueWrite(left); queueDestroy(left);return0;}Here is aMakefile.
ATuring machine is a hypothetical computational device that consists of a little trolley, called thecontroller, that rolls around on an infinite paper tape on which it can write letters. The controller itself has a small number of states that affect its behavior, and a program that tells it what to do when it is in a particular state and sees a particular letter.
In C terms, we can think of a Turing machine as consisting of an infinite array ofchars (representing the tape), an integer index into the array (representing the current position of the controller), and anint (representing the current state of the controller). At each step, the machine
- or+ to move.If the new state is 0, the machine halts and takes no more steps.
For this assignment, you are to write a Turing machine simulator. The program for the machine will be supplied inargv;argv[i] will give the behavior of the machine when in statei, where the first three characters specify what to do if the machine sees an'a', the second three characters specify what to do if the machine sees a'b', and so on. Each of these three-letter groups will look like(new-symbol,direction,new-state), wherenew-symbol is the new symbol to write (a lower-case letter),direction is the direction to move ('+', meaning one position to the right, or'-', meaning one position to the left) andnew-state is the new state to go to (a digit). Your program should run this program starting in state 1 on some location in the middle of a tape initially made up entirely of'a' characters, and continue until the program enters the special halting state 0. It should then print the number of steps until this occurred.
You may assume that the program isargv is complete in the sense that it includes rules for any combination of state and symbol you will encounter while executing it. You are not required to detect if this assumption is violated.
The program
b+2a-0 a-1a-1gives instructions for what to do in state 1 (b+2a-0) and state 2 (a-1a-1). In state 1, if the controller reads ana, the tripleb+2 means that it should writeb, move right (+), and switch to state 2. If instead it reads ab, the triplea-0 means that it should writea, move left (-), and halt (0). In state 2, the machine always writesa, moves left, and switches to state 1.
Below is a depiction of this machine’s execution. It passes through 4 states (including both the initial state and the final halting state) using a total of 3 steps. The controller and its current state is shown above its current position on the tape at each point in time. To avoid having to put in infinitely long lines, only the middle three tape cells are shown.
1aaa 2aba 1aba0aaaYou should submit aMakefile and whatever source files are needed to generate a program./turing whenmake is called with no arguments. Theturing program should simulate a Turing machine as described above and print the number of steps that it takes until it halts in decimal format, followed by a newline. It should not produce any other output. For example, using the program above, your program should print 3:
$ ./turing b+2a-0 a-1a-13For more complex programs you may get different results. Here is a 3 state, 3 symbol program that runs for a bit longer:
$ ./turing b+2a-0c-3 b-3c+2b-2 b-1a+2c-192649163You may assume that tape symbols can always be represented by lowercase letters, that states can always be represented by single digits, and thatargv is in the correct format (although it may be worth including a few assertions in your program for safety, just in case).
Not all Turing machine programs will halt. Your program is not required to detect if the Turing machine it is simulating will halt eventually or not (although it should notice if it does halt).
Submit all files needed to build your program as usual using/c/cs223/bin/submit 5filename.
There is a public test script in/c/cs223/Hwk5/test.public. You can run this on your submitted files with/c/cs223/bin/testit 5 public.
/* * Simple Turing machine simulator. * * Tape holds symbols 0 (default) through 2. * * Controller programming is specified in argv: * * argv[i] gives transitions for state i as six characters. * * Each triple of characters is <action><direction><new-state> * * where <action> is one of: * * a,b,c: write this value to tape * * <direction> is one of: * * -: go left * +: go right * .: stay put * * The three pairs give the transition for reading 0, 1, 2 from tape. * * State 0 is the halting state. * * On halting, prints number of transitions followed by contents * of all tape cells that have ever been visited by the * finite-state controller. */#include<stdio.h>#include<stdlib.h>#include<string.h>#include<assert.h>#include<sys/types.h>struct configuration {unsignedint state;/* state of head */size_t leftmost;/* leftmost cell visited */size_t rightmost;/* rightmost cell visited */size_t current;/* current cell */size_t tapeLength;/* current allocated space for tape */char *tape;/* contents of cells */};/* increase the size of the tape and recenter contents in middle */voidconfigurationExpand(struct configuration *c){size_t newTapeLength;char *oldTape;char *newTape;size_t i;ssize_t offset; newTapeLength =4*c->tapeLength; newTape = malloc(newTapeLength); assert(newTape);for(i =0; i < newTapeLength; i++) { newTape[i] =0; }/* copy old tape */ offset = newTapeLength /2 - c->current;for(i = c->leftmost; i <= c->rightmost; i++) { newTape[i + offset] = c->tape[i]; } oldTape = c->tape; c->tape = newTape; c->tapeLength = newTapeLength; c->current += offset; c->leftmost += offset; c->rightmost += offset; free(oldTape);}#define INITIAL_TAPE_LENGTH (16)struct configuration *configurationCreate(void){struct configuration *c;size_t i; c = malloc(sizeof(struct configuration)); assert(c); c->state =1; c->tapeLength = INITIAL_TAPE_LENGTH; c->leftmost = c->rightmost = c->current = c->tapeLength /2; c->tape = malloc(c->tapeLength); assert(c->tape);for(i =0; i < c->tapeLength; i++) { c->tape[i] =0; }return c;}voidconfigurationDestroy(struct configuration *c){ free(c->tape); free(c);}#define SYMBOL_BASE ('a')#define STATE_BASE ('0')/* used for debugging mostly */voidconfigurationPrint(conststruct configuration *c){size_t i;for(i = c->leftmost; i < c->current; i++) { putchar(' '); } putchar(STATE_BASE + c->state); putchar('\n');for(i = c->leftmost; i <= c->rightmost; i++) { putchar(SYMBOL_BASE + c->tape[i]); } putchar('\n');}intmain(int argc,char **argv){struct configuration *c;char cellValue;constchar *transition;size_t steps;if(argc ==1) { fprintf(stderr,"Usage: %s transitions\n", argv[0]);return1; } c = configurationCreate(); steps =0;while(c->state !=0) { steps++;/* execute the next transition */ assert(c->state < argc); cellValue = c->tape[c->current]; assert(0 <= cellValue); assert(3*(cellValue+1) <= strlen(argv[c->state])); transition = argv[c->state] +3*c->tape[c->current]; c->tape[c->current] = transition[0] - SYMBOL_BASE;switch(transition[1]) {case'-':if(c->current ==0) { configurationExpand(c); } c->current--;if(c->current < c->leftmost) { c->leftmost = c->current; }break;case'+':if(c->current == c->tapeLength -1) { configurationExpand(c); } c->current++;if(c->current > c->rightmost) { c->rightmost = c->current; }break;case'.':/* do nothing */break;default: fprintf(stderr,"Bad direction '%c'\n", transition[2]); exit(2);break; } c->state = transition[2] - STATE_BASE;#ifdef PRINT_CONFIGURATION configurationPrint(c);#endif }/* print number of steps */ printf("%zu\n", steps); configurationDestroy(c);return0;}CC=gccCFLAGS=-std=c99 -Wall -pedantic -g3all: turingcompositor: turing.o$(CC)$(CFLAGS) -o$@$^clean:$(RM) turing *.oFor this assignment, you are to implement a data structure for playing a game involving ships placed in a large square grid. Each ship occupies one more more squares in either a vertical or horizontal line, and has a name that consists of a singlechar other than a period (which will be used to report the absence of a ship). Ships have a bounded maximum length; attempts to place ships longer than this length have no effect.
All type and constant definitions for the data type, and all function declarations, are given in the fileships.h, which is shownbelow, and which you can also find in/c/cs223/Hwk6/sourceFiles/ships.h. The playing field is represented by astruct field (which you get to define). A newstruct field is created byfieldCreate, and when no longer needed should be destroyed byfieldDestroy.
These data types fromships.h control ship naming and placement. Note thatuint32_t is defined instdint.h (which is also included byinttypes.h. You will need to include one of these files beforeships.h to get this definition.
typedefuint32_t coord;struct position { coord x; coord y;};struct ship {struct position topLeft;/* coordinates of top left corner */int direction;/* HORIZONTAL or VERTICAL */unsignedint length;/* length of ship */char name;/* name of ship */};Actual placement is done using thefieldPlaceShip function, declared as follows:
A ship of lengthm placed horizontally with its top left corner at position(x, y) will occupy positions(x, y) through(x + m − 1, y). If instead it is placed vertically, it will occupy positions(x, y) through(x, y + m − 1). If any of these coordinates exceed the maximum coordinateCOORD_MAX (defined inships.h), the ship will not be placed. The ship will also not be placed if itsname field is equal toNO_SHIP_NAME or if the length exceedsMAX_SHIP_LENGTH.
If the new ship will occupy any position as a ship previously placed in the field, the previous ship will be removed. It is possible for many ships to be removed at once in this way.
ThefieldAttack function can be used to remove a ship at a particular location without placing a new ship. It returns the name of the removed ship, if any, orNO_SHIP_NAME if there is no ship at that location.
Finally, thefieldCountShips returns the number of ships still present in the field.
Your job is to write an implementation of these functions, which you should probably put in a fileships.c. You must also supply aMakefile, which, whenmake is called with no arguments, generates a test programtestShips from your implementation and the filetestShips.c that we will provide. You should not count on precisely this version oftestShips.c being supplied; your implementation should work with any main program that respects the interface inships.h.
You should write your implementation so that it will continue to work if thetypedef forcoord, or the definitions of the constantsCOORD_MAX,NO_SHIP_NAME,SHIP_MAX_LENGTH,HORIZONTAL, orVERTICAL change. You may, however, assume thatcoord is an unsigned integer type and theCOORD_MAX is the largest value that can be represented by this type.
If it helps in crafting your implementation, you may assume thatMAX_SHIP_LENGTH will alway be a reasonably small constant. You do not need to worry about implementing a data structure that will handle huge ships efficiently. On the other hand,COORD_MAX as defined in the defaultships.h is232 − 1, so you will need to be able to deal with a field with at least264 possible locations, a consideration you should take into account when choosing a data structure to represent a field.
testShips programThe suppliedtestShips program creates a field, processes commands fromstdin, then destroys the field when it reachesEOF or a command that it can’t parse. Each command is either of the form
+ x y vertical length namewhich means to callfieldPlaceShip for a new ship at coordinates (x,y), with directionHORIZONTAL ifisVertical is false andVERTICAL otherwise, and lengthlength and namename.
The command
- x ycallsfieldAttack on coordinates (x,y).
Here is a small input file:
+ 0 0 0 3 a+ 1 2 0 4 b+ 3 0 1 3 c- 1 0After processing the first line, the contents of the field should look like this:
aaa......................After processing the second line:
aaa........bbbb..........The third line places a ship vertically that intersects one of the previous ships, sinking it:
aaac....c....c...........Had shipc been shifted one position to the left, it would have sunkena as well.
Finally, the last line drops a bomb at(1, 0), sinkinga:
...c....c....c...........The input files used bytest.public can be found in/c/cs223/Hwk6/testFiles. Some of these were generated randomly using the script/c/cs223/Hwk6/makeRandom, which you should feel free to use for your own nefarious purposes.
Because the interface inships.h gives no way to find out what ships are currently in the field, the test program will not actually produce pictures like the above. Instead, it prints after each command a line giving the name of the ship sunken byfieldAttack (orNO_SHIP_NAME if no ship is sunk orfieldPlaceShip is called) and the number of ships left in the field following the attack. So the user must imagine the carnage as the 100000 ships inrandomSparseBig.in somehow leave only 25336 survivors inrandomSparseBig.out, demonstrating the importance of strict navigational rules in real life.
Submit your assignment as usual with
/c/cs223/bin/submit 6You can run the public test script in/c/cs223/Hwk6/test.public on your submitted files with
/c/cs223/bin/testit 6 public#define HORIZONTAL (0)/* place ship horizontally */#define VERTICAL (1)/* place ship vertically */#define MAX_SHIP_LENGTH (17)/* length of longest ship (width is always 1) */#define NO_SHIP_NAME ('.')/* what to return when hitting no ship *//* * Type for coordinates, and their maximum possible value. * * Include <stdint.h> before this header file * to get the definition of uint32_t * and its maximum value UINT32_MAX. */typedefuint32_t coord;#define COORD_MAX (UINT32_MAX)/* * Non-opaque structs for passing around positions and ship placements. */struct position { coord x; coord y;};struct ship {struct position topLeft;/* coordinates of top left corner */int direction;/* HORIZONTAL or VERTICAL */unsignedint length;/* length of ship */char name;/* name of ship */};/* * Create a playing field for holding ships. */struct field *fieldCreate(void);/* * Free all space associated with a field. */void fieldDestroy(struct field *);/* * Place a ship in a field with given placement and name. * * If placement.length is less than one or greater than MAX_SHIP_LENGTH, * or if some part of the ship would have a coordinate greater than COORD_MAX, * or if the ship's name is NO_SHIP_NAME, * the function returns without placing a ship. * * Placing a new ship that intersects any previously-placed ships * sinks the previous ships, removing them from the field. */void fieldPlaceShip(struct field *f,struct ship s);/* * Attack! * * Drop a shell at given position. * * Returns NO_SHIP_NAME if attack misses (does not intersect any ship). * * Otherwise returns name of ship hit. * * Hitting a ship sinks it, removing it from the field. */char fieldAttack(struct field *f,struct position p);/* * Return number of ships in the field. */size_t fieldCountShips(conststruct field *f);#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<stdint.h>#include<inttypes.h>#include"ships.h"#define PLACE_SHIP ('+')/* command to place a new ship */#define ATTACK ('-')/* command to attack a location */intmain(int argc,char **argv){struct field *f;/* where we keep our ships */int command;/* command char */struct ship s;/* ship we are placing */struct position p;/* location to attack */int sank;/* ship we sank */if(argc !=1) { fprintf(stderr,"Usage: %s\n", argv[0]);return1; } f = fieldCreate();while((command = getchar()) != EOF) {switch(command) {case PLACE_SHIP:if(scanf("%" SCNu32" %" SCNu32"%d %u %c ", &s.topLeft.x, &s.topLeft.y, &s.direction, &s.length, &s.name) !=5) {/* not enough args */ fprintf(stderr,"Not enough enough args to %c\n", PLACE_SHIP);return1; }/* else *//* fix the direction to match actual definitions */ s.direction = s.direction ? VERTICAL : HORIZONTAL; fieldPlaceShip(f, s); sank = NO_SHIP_NAME;break;case ATTACK:if(scanf("%" SCNu32" %" SCNu32" ", &p.x, &p.y) !=2) { fprintf(stderr,"Not enough enough args to %c\n", ATTACK);return1; }/* else */ sank = fieldAttack(f, p);break;default:/* bad command */ fprintf(stderr,"Bad command %c\n", command);return1;break; } printf("%c %zu\n", sank, fieldCountShips(f)); } fieldDestroy(f);return0;}#include<stdlib.h>#include<assert.h>#include<string.h>#include<stdint.h>#include"ships.h"/* basic hash table */struct field {size_t size;/* number of slots in table */size_t occupancy;/* number of elements in table */struct elt **table;/* hash table, malloc'd */};struct elt {struct elt *next;/* pointer to next element in linked list */struct ship ship;/* ship in this element */};/* picked more or less at whim from http://planetmath.org/goodhashtableprimes */#define X_HASH_FACTOR (201326611)#define Y_HASH_FACTOR (3145739)staticsize_thash(struct position p){return X_HASH_FACTOR * p.x + Y_HASH_FACTOR * p.y;}#define DEFAULT_INITIAL_SIZE (8)/* like fieldCreate, but argument gives initial size */staticstruct field *fieldCreateInternal(size_t initialSize){struct field *f;size_t i; f = malloc(sizeof(struct field)); assert(f); f->size = initialSize; f->occupancy =0; f->table = malloc(sizeof(struct elt *) * f->size);for(i =0; i < f->size; i++) { f->table[i] =0; }return f;}struct field *fieldCreate(void){return fieldCreateInternal(DEFAULT_INITIAL_SIZE);}/* destroy contents of f but don't free f itself */staticvoidfieldDestroyContents(struct field *f){size_t i;struct elt *e;struct elt *next;for(i =0; i < f->size; i++) {for(e = f->table[i]; e !=0; e = next) { next = e->next; free(e); } } free(f->table);}voidfieldDestroy(struct field *f){ fieldDestroyContents(f); free(f);}/* when to grow field */#define MAX_ALPHA (1)/* * Helper for fieldPlaceShip. * * This skips all the sanity-checking in fieldPlaceShip, * and just performs the hash table insertion. */staticvoidfieldInsertShip(struct field *f,struct ship s){size_t h;/* hashed coordinates */struct elt *e;/* new element to insert */ h = hash(s.topLeft) % f->size; e = malloc(sizeof(struct elt)); assert(e); e->ship = s; e->next = f->table[h]; f->table[h] = e; f->occupancy++;}voidfieldPlaceShip(struct field *f,struct ship s){struct field *f2;struct elt *e;struct position pos;size_t i;/* test if we can just throw this away */if(s.name == NO_SHIP_NAME || s.length ==0 || s.length > MAX_SHIP_LENGTH || (s.direction == HORIZONTAL && s.topLeft.x > COORD_MAX - (s.length -1)) || (s.direction == VERTICAL && s.topLeft.y > COORD_MAX - (s.length -1)) ) {return; }/* else */if(f->occupancy >= f->size * MAX_ALPHA) {/* grow the field */ f2 = fieldCreateInternal(f->size *2);/* copy to new field */for(i =0; i < f->size; i++) {for(e = f->table[i]; e !=0; e = e->next) {/* skip testing for occupancy or intersections */ fieldInsertShip(f2, e->ship); } }/* transplant new field into old field */ fieldDestroyContents(f); *f = *f2; free(f2); }/* check for intersections */ pos = s.topLeft;for(i =0; i < s.length; i++) {if(s.direction == HORIZONTAL) { pos.x = s.topLeft.x + i; }else { pos.y = s.topLeft.y + i; } fieldAttack(f, pos); }/* call helper to do the actual hash table insertion */ fieldInsertShip(f, s);}/* * Helper for fieldAttack. * * If there is a ship with topLeft at given position, return pointer * to location in hash table that points to it (either table entry * or next component). * * If not, return null. */staticstruct elt **fieldShipAt(struct field *f,struct position p){struct elt **prev;/* previous pointer */for(prev = &f->table[hash(p) % f->size]; *prev !=0; prev = &((*prev)->next)) {if((*prev)->ship.topLeft.x == p.x && (*prev)->ship.topLeft.y == p.y) {return prev; } }/* didn't find anything */return0;}/* * Attack! * * Drop a shell at given position. * * Returns 0 if attack misses (does not intersect any ship). * * Otherwise returns name of ship hit, * which should be freed by caller when no longer needed. * * Hitting a ship sinks it, removing it from the field. */charfieldAttack(struct field *f,struct position p){struct position p2;int i;int direction;struct elt **prev;struct elt *freeMe;char name;for(direction =0; direction <=1; direction++) {for(i =0; i < MAX_SHIP_LENGTH && i <= (direction == HORIZONTAL ? p.x : p.y); i++) {if(direction == HORIZONTAL) { p2.x = p.x - i; p2.y = p.y; }else { p2.x = p.x; p2.y = p.y - i; } prev = fieldShipAt(f, p2);if(prev) {/* if we sink anybody, it will be this ship *//* but maybe it doesn't reach *//* or points in the wrong direction */if((*prev)->ship.length > i && (*prev)->ship.direction == direction) {/* got it */ freeMe = *prev; *prev = freeMe->next; name = freeMe->ship.name; free(freeMe); f->occupancy--;return name; }else {/* didn't get it *//* maybe try again in other direction */break; } } } }/* didn't get anything */return NO_SHIP_NAME;}/* * Return number of ships in the field. */size_tfieldCountShips(conststruct field *f){return f->occupancy;}For this assignment you are to implement a strategy for playing a card game involving moving cards (represented byuint64_ts) down through a sequence ofn piles. The interface to your strategy is given in the filestrategy.h, shown below:
/* * Interface for card-playing strategy. * * The deal function supplies a new card to the strategy. Each possible card will only be dealt once. * * The play function should return a card that has been dealt previously but not yet played. * If asked for a card when the hand is empty, its behavior is undefined. */#include<stdint.h>typedefuint64_t Card;/* representation of a card *//* opaque type for strategy data */typedefstruct strategy Strategy;/* set up a new strategy for numPiles many piles */Strategy *strategyCreate(int numPiles);/* clean up all space used by a strategy */void strategyDestroy(Strategy *);/* add a card to the current hand */void strategyDeal(Strategy *, Card);/* play a card from pile k */Card strategyPlay(Strategy *,int k);Initially, the player hasn piles, numbered1 throughn. ThestrategyDeal function is called to indicate that a new card has been dealt to pilen. ThestrategyPlay function is called to indicate that a card should be moved from pilek to pilek-1; this function should return the card to move. Cards moved to pile0 leave the game and are not used again. Each card is unique: once a card is dealt, the same card will never be dealt again during the same play of the game.
The choice of when to deal and when to play from pile is controlled by some external entity, which at some point will stop and compute the smallest card in each pile. The goal of the strategy is to make these smallest cards be as large as possible, giving priority to the highest-numbered piles: given two runs of the game, the better-scoring one is the one that has the larger smallest card in pilen, or, if both have the same smallest card in pilen, the one that has the larger smallest card in pilen − 1, and so forth. A tie would require that both runs end with the same smallest card in every pile. An empty pile counts asUINT64_MAX for this purpose (although note that a strategy has no control over which piles are empty).
Your job is to implement a strategy that produces the best possible result for any sequence of calls tostrategyDeal andstrategyPlay. Your strategy implementation will most likely need to keep track of which cards are available in each pile, as this information is not provided by the caller. YourstrategyPlay function should only make legal moves: that is, it should only play cards that are actually present in the appropriate pile. You may assume thatstrategyPlay is never called on an empty pile.
Your implementation should consist of a filestrategy.c and any supporting source and header files that you need other thanstrategy.h, which we have provided for you. You should also supply a fileMakefile that generates a programtestStrategy whenmake is called with no arguments, using your implementation and thetestStrategy.c file that you can find in/c/cs223/Hwk7/sourceFiles/testStrategy.c.
ThetestStrategy program implements one of four rules for when you can play from each pile. The arguments totestStrategy are a character indicating which rule to apply, the number of cards to deal (which can be pretty big), and the number of piles (which is much more limited, becausetestStrategy.c tracks the pile each card is in using achar to save space). The actual cards dealt are generated deterministically and will be the same in every execution with the same arguments. The test files in/c/cs223/Hwk7/testFiles give the expected output whentestStrategy is run with the arguments specified in the filename (after removing the- characters); this will always be the value, in hexadecimal, of the smallest card in each pile, starting with the top pile.
For example, running theharmonic ruleh with 1000 cards and 4 piles (not counting the 0 pile) gives the output
$ ./testStrategy h 1000 45462035faf0d6fa1501ebb6268d39af325732b5fee7c8ad7301e0f608d124edeThis output would appear in a filenameh-1000-4, if this particular combination of parameters were one of the test cases.
Submit your assignment as usual with/c/cs223/bin/submit 7. You should submit your source file(s), yourMakefile, and any other files needed to build your program other thanstrategy.h andtestStrategy.c, which will be supplied by the test script. You can test your submission using the public test script in/c/cs223/Hwk7/test.public using the command/c/cs223/bin/testit 7 public.
I implemented a heap withuint64_t elements as a separate module (heap.h,heap.c) and then used it in a main modulestrategy.c that allocates a separate heap for each pile and manages the translation betweenstrategyDeal andstrategyPlay and the heap functions. TheMakefile is pretty much the usual.
For this assignment, you are to implement an ordered set data type for holding null-terminated strings. The interface to this data type is given in the fileorderedSet.h, shown below.
/* * Ordered set data structure. *//* Make a new empty set */struct orderedSet *orderedSetCreate(void);/* Destroy a set */void orderedSetDestroy(struct orderedSet *);/* How many elements in this set? */size_t orderedSetSize(conststruct orderedSet *);/* Insert a new element. Has no effect if element is already present. */void orderedSetInsert(struct orderedSet *,constchar *);/* Delete an element. Has no effect if element is not already present. */void orderedSetDelete(struct orderedSet *,constchar *);/* Return a new ordered set containing all elements e * for which predicate(arg, x) != 0. * The predicate function should be applied to the elements in increasing order. */struct orderedSet *orderedSetFilter(conststruct orderedSet *,int (*predicate)(void *arg,constchar *),void *arg);In addition to the usual create and destroy functions, an ordered set supports inserting and deleting elements, counting the number of distinct elements in the set, and filtering the set based on a predicate function passed in as an argument. This filtering operation does not modify the input set, but instead generates a new ordered set containing only those elements on which the predicate returns a nonzero value.
The filtering operation is where most of the excitement happens; because the predicate function takes an argument of typevoid * that is also passed to the filter function, it is possible for the predicate to compute an arbitrary function on the elements of the set as a side-effect of processing each element to decide whether to put it in the output set. This allows predicates to be abused to perform all sorts of computations, including printing out all elements of the set or computing a hash of all the strings in the set concatenated together. These features are used by the test programtestOrderedSet.c that we have provided. To ensure that these traversals give consistent results, it is required that when your implementation oforderedSetFilter is executed, it calls the predicate function exactly once on each element of the set in increasing order as determined bystrcmp.
testOrderedSet wrapperThe test program is a fairly thin wrapper over the implementation that allows you to call the various functions using one-line commands on standard input. A command is given as the first character of the line, and the rest of the line contains the argument to the command if needed. The+ and- commands add or remove an element from the set, respectively, while thep,s, andh commands print the contents of the set, the size of the set, and a hash of the set (these commands ignore any argument). Thef command removes all elements of the set that do not contain a particular substring.
Here is a simple input to the program that inserts four strings, filters out the ones that don’t containee, then prints various information about the results.
+feed+the+bees+pleasefeeshpThis should produce the output
215082778b3db8cb3beesfeedSubmit, with the usual/c/cs223/bin/submit 8 filename, yourMakefile and any supporting files needed to build the programtestOrderedSet fromtestOrderedSet.c andorderedSet.h whenmake is called with no arguments. These last two files will be provided by the test script and you do not need to submit them.
You can test your submission against the public test script in/c/cs223/Hwk8/test.public with/c/cs223/bin/testit 8.
There were a lot of ways to do this. For the sample solution, I decided to do something unusual, and store the set as a hash table. This is not ordered, but since the only operation that requires the set to be ordered isorderedSetFilter, which will takeΩ(n) time no matter how you implement it, theO(nlog n) cost to callqsort to sort the elements as needed does not add much overhead.
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<stdint.h>#include<string.h>#include"orderedSet.h"/* We'll use a hash table with linear probing. * This is not actually ordered, but the only operations that * depend on order a linear-time anyway, so we can afford to sort as needed */struct orderedSet {size_t n;/* number of elements */size_t size;/* size of the table */char **table;/* hash table */};#define INITIAL_SIZE (16)#define MAX_ALPHA (0.75)/* Make a new empty set with given size */staticstruct orderedSet *orderedSetCreateInternal(size_t size){struct orderedSet *s; s = malloc(sizeof(*s)); assert(s); s->n =0; s->size = size; s->table = calloc(s->size,sizeof(char *));return s;}struct orderedSet *orderedSetCreate(void){return orderedSetCreateInternal(INITIAL_SIZE);}/* Destroy a set */voidorderedSetDestroy(struct orderedSet *s){size_t i;for(i =0; i < s->size; i++) {if(s->table[i]) { free(s->table[i]); } } free(s->table); free(s);}/* How many elements in this set? */size_torderedSetSize(conststruct orderedSet *s){return s->n;}staticsize_thash(constchar *s){size_t h;/* usual crummy hash function */for(h =0; *s; h = h *97 + *s++);return h;}staticchar *strMalloc(constchar *s){char *s2; s2 = malloc(strlen(s)+1); strcpy(s2, s);return s2;}/* Insert and element without doing size check or malloc *//* Frees element if already present */staticvoidorderedSetInsertInternal(struct orderedSet *s,char *elt){size_t h; assert(elt);/* skip over non-empty slots with different values */for(h = hash(elt) % s->size; s->table[h] && strcmp(s->table[h], elt); h = (h+1) % s->size);/* check if not already present */if(s->table[h] ==0) { s->table[h] = elt; s->n++; }else { free(elt); }}/* Insert a new element. Has no effect if element is already present. */voidorderedSetInsert(struct orderedSet *s,constchar *elt){size_t h;struct orderedSet *s2;if(s->n >= s->size * MAX_ALPHA) {/* rebuild the table */ s2 = orderedSetCreateInternal(s->size *2);/* copy all the elements */for(h =0; h < s->size; h++) {if(s->table[h]) { orderedSetInsertInternal(s2, s->table[h]); } }/* free the table and then do a brain transplant */ free(s->table); *s = *s2; free(s2); } orderedSetInsertInternal(s, strMalloc(elt));}/* Delete an element. Has no effect if element is not already present. */voidorderedSetDelete(struct orderedSet *s,constchar *elt){size_t h;char *later;/* skip over non-empty slots with different values */for(h = hash(elt) % s->size; s->table[h] && strcmp(s->table[h], elt); h = (h+1) % s->size);/* if we reached a nonempty slot, it must be our target */if(s->table[h] !=0) {/* remove the initial element */ free(s->table[h]); s->table[h] =0; s->n--;/* remove and reinsert any elements up to the next hole, in case they wanted to be earlier */for(h = (h+1) % s->size; s->table[h] ; h = (h+1) % s->size) { later = s->table[h]; s->table[h] =0; s->n--; orderedSetInsertInternal(s, later); } }}staticintcompare(constvoid *s1,constvoid *s2){return strcmp(*((constchar **) s1), *((constchar **) s2));}/* Return a new ordered set containing all elements e * for which predicate(arg, x) != 0. * The predicate function should be applied to the elements in increasing order. */struct orderedSet *orderedSetFilter(conststruct orderedSet *s,int (*predicate)(void *arg,constchar *),void *arg){size_t h;constchar **a;/* temporary array to sort */size_t top;/* where to put things in a */size_t i;struct orderedSet *s2; a = malloc(sizeof(constchar *) * s->size); assert(a); top =0;for(h =0; h < s->size; h++) {if(s->table[h]) { a[top++] = s->table[h]; } } qsort(a, top,sizeof(constchar *), compare); s2 = orderedSetCreate();for(i =0; i < top; i++) {if(predicate(arg, a[i])) { orderedSetInsert(s2, a[i]); } } free(a);return s2;}Makefile{examples/2015/hw/8/Makefile}
For this problem, you are given a rectangular maze consisting ofwall squares (represented by 0) andpath squares (represented by 1). Two path squares are considered to be adjacent if they are at most one square away orthogonally or diagonally; in chess terms, two path squares are adjacent if a king can move from one to the other in one turn. The input to your program is a maze in which the graph consisting of all path squares is connected and contains at most one cycle, where a cycle is a sequence of distinct squaress1, s2, …, sk where eachsi is adjacent tosi + 1 andsn is adjacent tos1. Your job is to write a programmaze that finds this cycle if it exists, and marks all of its squares ascycle squares (represented by 2).
For example, here is a picture of a 200-by-100 maze that contains a small cycle:
and here is the same maze with the cycle highlighted in white:
The input to your program should be taken fromstdin, in a restricted version ofraw PGM format, an old image format designed to be particularly easy to parse. The input file header will be a line that looks like it was generated by theprintf conversion string"P5 %d %d 255\n", where the firstint value is the width of the image in columns and the second is the height of the image in rows; the same conversion string can be given toscanf to parse this line. Following the newline will be a long sequence of bytes, each representing one pixel of the image, with each row following immediately after the previous one. These bytes will be either 0 or 1 depending on whether that position in the maze is a wall or a path.
The output to your program should be in the same format, with the difference that now some of the bytes in the image data may be 2, indicating the cycle. If there is no cycle, the output should be identical to the input. Your program is not required to detect or respond in any particular way to input mazes that violate the format or do not contain a connected graph of path squares, although you are encouraged to put in reasonable error checking for your own benefit during testing.
For example, the maze depicted above is stored in the file200-100-4.in.pgm; the corresponding output is stored in the file200-100-4.out.pgm. Other sample inputs and outputs can be found in/c/cs223/Hwk9/testFiles.
This file format is hard to read with the naked eye, even after loading into a text editor. The script/c/cs223/Hwk9/toPng will generate a PNG file that doubles the pixel size and rescales the 0, 1, 2 pixel values to more reasonable values for display. This can be called as/c/cs223/Hwk9/toPng filename.pgm to produce a new filefilename.pgm.png. This works best iffilename.pgm is already in a directory you can write to. PNG files can be displayed using most web browsers and image manipulation tools.
Submit whatever files you need to buildmaze (including aMakefile that generatesmaze when called with no arguments) using/c/cs223/bin/submit 9. You can apply the public test script in/c/cs223/Hwk9/test.public to your submitted files using/c/cs223/bin/testit 9 public.
This uses breadth-first search, which makes the search a bit simpler than depth-first search but requires some more effort to compute the cycle. The program also includes code for generating random mazes.
#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<math.h>#include<limits.h>struct direction {signedchar x;signedchar y;};#define DIRECTIONS (8)conststruct direction directions[DIRECTIONS] = { { -1, -1 }, { -1,0 }, { -1,1 }, {0, -1 }, {0,1 }, {1, -1 }, {1,0 }, {1,1 }};struct position {int x;int y;};conststruct position NO_POSITION = { -1, -1 };staticinlineinteqPosition(struct position p,struct position q){return p.x == q.x && p.y == q.y;}#define WALL (0)#define PATH (1)#define CYCLE (2)struct square {int contents;struct position parent;/* used by search routine */};struct maze {struct position size;/* rows = size.x, columns = size.y */struct square *a;/* packed array of squares */};/* look up a position in a maze */#define Mref(m, pos) ((m)->a[(pos).y * (m)->size.x + (pos).x])#define Mget(m, pos) (assert((pos).x >= 0 && (pos).y >= 0 && (pos).x < (m)->size.x && (pos).y < (m)->size.y), Mref((m), (pos)))/* add direction to source to get target *//* returns 1 if target is in range */intoffset(conststruct maze *m,struct position *target,struct position source,struct direction dir){ target->x = source.x + dir.x; target->y = source.y + dir.y;return target->x >=0 && target->y >=0 && target->x < m->size.x && target->y < m->size.y;}/* free a maze */voiddestroyMaze(struct maze *m){ free(m->a); free(m);}/* load a maze in restricted PGM format */struct maze *loadMaze(FILE *f){struct maze *m;struct position i; m = malloc(sizeof(*m)); assert(m); fscanf(f,"P5 %d %d 255\n", &m->size.x, &m->size.y); m->a = malloc(sizeof(struct square) * m->size.y * m->size.x);for(i.y =0; i.y < m->size.y; i.y++) {for(i.x =0; i.x < m->size.x; i.x++) { Mref(m, i).contents = getchar(); assert(Mref(m, i).contents ==0 || Mref(m, i).contents ==1); } }return m;}voidsaveMaze(struct maze *m,FILE *f){struct position i; fprintf(f,"P5 %d %d 255\n", m->size.x, m->size.y);for(i.y =0; i.y < m->size.y; i.y++) {for(i.x =0; i.x < m->size.x; i.x++) { putc(Mref(m, i).contents, f); } }}/* how many neighbors of position are PATH? */intcountNeighbors(conststruct maze *m,struct position p){struct position q;int i;int count =0;for(i =0; i < DIRECTIONS; i++) {if(offset(m, &q, p, directions[i]) && Mget(m, q).contents == PATH) { count++; } }return count;}struct positionrandomPosition(conststruct maze *m){struct position r; r.x = rand() % m->size.x; r.y = rand() % m->size.y;return r;}#define PATIENCE_MULTIPLIER (4)/* generate a random connected maze with no cycles */struct maze *generateMaze(struct position size){struct maze *m;struct position r;struct position i;size_t countdown;/* how long to run before we get tired of not making progress */size_t maxCountdown;/* value to reset countdown to when we make progress */ m = malloc(sizeof(struct maze)); assert(m); m->size = size; m->a = malloc(sizeof(struct square) * m->size.x * m->size.y); assert(m->a);/* start with all WALL */for(i.y =0; i.y < m->size.y; i.y++) {for(i.x =0; i.x < m->size.x; i.x++) { Mref(m, i).contents = WALL; } }/* place a PATH on a random square */ r = randomPosition(m); Mref(m, r).contents = PATH; maxCountdown = PATIENCE_MULTIPLIER * size.x * size.y * log(size.x * size.y);for(countdown = maxCountdown; countdown >0; countdown--) {/* pick a random square */ r = randomPosition(m);/* add if we have exactly one neighbor already in the maze */if(Mget(m, r).contents == WALL && countNeighbors(m, r) ==1) { Mref(m, r).contents = PATH;/* reset countdown */ countdown = maxCountdown; } }return m;}/* create a cycle by adding one extra PATH square * that connects two existing squares */voidmazeAddCycle(struct maze *m){struct position r;do { r = randomPosition(m); }while(Mget(m, r).contents != WALL || countNeighbors(m, r) !=2); Mref(m, r).contents = PATH;}/* Search for a cycle of PATH nodes. * If found, mark all nodes on the cycle as CYCLE. */voidmazeSearchForCycle(struct maze *m){struct position root;/* root of tree */struct position current;/* what we just popped */struct position parent ;/* current's parent */struct position neighbor;/* neighbor to push */struct position ancestor;/* for filling in CYCLE */int i;struct position *queue;size_t head;/* where to dequeue */size_t tail;/* where to enqueue *//* this is probably more space than we need */ queue = malloc(sizeof(struct position) * m->size.x * m->size.y); assert(queue); head = tail =0;/* clear out bookkeeping data */for(current.y =0; current.y < m->size.y; current.y++) {for(current.x =0; current.x < m->size.x; current.x++) { Mref(m, current).parent = NO_POSITION;/* probably not necessary but will avoid trouble * if somebody calls this twice */if(Mget(m, current).contents != WALL) { Mref(m, current).contents = PATH; } } }/* find a root *//* we don't care what this is, but it can't be a WALL */do { root = randomPosition(m); }while(Mget(m, root).contents != PATH);/* push root */ Mref(m, root).parent = root; queue[tail++] = root;/* now perform the BFS *//* if we ever find a neighbor that is already in the tree and not our parent, * we have found our cycle */while(head < tail) { current = queue[head++]; parent = Mget(m, current).parent;/* push all neighbors not already in tree *//* if one is in the tree, we win */for(i =0; i < DIRECTIONS; i++) {if(offset(m, &neighbor, current, directions[i]) && Mget(m, neighbor).contents == PATH && !eqPosition(neighbor, parent)) {/* is it already in the tree? */if(!eqPosition(Mget(m, neighbor).parent, NO_POSITION)) {/* we win *//* cycle consists of all ancestors of neighbor and current * up to common ancestor */for(ancestor = neighbor; !eqPosition(ancestor, root); ancestor = Mget(m, ancestor).parent) { Mref(m, ancestor).contents = CYCLE; }/* also mark root */ Mref(m, root).contents = CYCLE;/* now work up from current */for(ancestor = current; !eqPosition(ancestor, root); ancestor = Mget(m, ancestor).parent) {if(Mget(m, ancestor).contents == PATH) {/* add to the cycle */ Mref(m, ancestor).contents = CYCLE; }else {/* this is the common ancestor, which is not root *//* mark all proper ancestors as PATH */do { ancestor = Mget(m, ancestor).parent; Mref(m, ancestor).contents = PATH; }while(!eqPosition(ancestor, root));/* can't just break, too many loops */goto doneWithSearch; } } }else { Mref(m, neighbor).parent = current; queue[tail++] = neighbor; } } } }doneWithSearch: free(queue);}intmain(int argc,char **argv){struct maze *m;struct position size = {80,60 };int seed;switch(argc) {case1:/* sample solution for the assignment */ m = loadMaze(stdin); mazeSearchForCycle(m); saveMaze(m, stdout); destroyMaze(m);break;case4:/* generate a new test image *//* usage is ./maze width height seed *//* if seed is negative, use absolute value and don't put in cycle */ size.x = atoi(argv[1]); size.y = atoi(argv[2]); seed = atoi(argv[3]); srand(seed <0 ? -seed : seed); m = generateMaze(size);if(seed >=0) { mazeAddCycle(m); } saveMaze(m, stdout); destroyMaze(m);break;default: fprintf(stderr,"Usage %s or %s width height seed\n", argv[0], argv[0]);return1; }return0;}And theMakefile.
Here are some notes from a helpful Zoo denizen about debugging programs in 223. (Note: these have been edited slightly from the original.)
Date: Thu, 10 Feb 2005 06:02:23 -0500 (EST)From: James Terry <james.c.terry@yale.edu>Subject: 223 coding feedbackHi Jim,Several of your students for 223 were up late last night in the Zooworking on their assignments, and they seemed to be getting hung up onsome coding issues. They were pretty frustrated with some standardlanguage/debugging issues, so I helped them get the type-checker andValgrind to stop yelling at them. I noticed some recurring problems and Ithought I'd pass them on to you. They're pretty standard mistakes, andI've made most of them myself at some point, either in your class or inStan's. It occurred to me that there might be more confused people thanwere around last night, and they'd probably appreciate it if someone toldthem about these sort of things. I'm not trying to intrude on how youteach the class; I just thought this feedback would be helpful and Iwasn't sure that it would find its way to you otherwise. I'm sure you'vealready taught them several of these, and I understand that sometimesstudents just don't pay attention. Still, these seem like good points tohammer down:Recurring debugging/coding problems:1. If you want a debugger/Valgrind to give you line numbers, you must compile with debugging info turned on, i. e. using the -g[level] flag.2. On the Zoo, pointers and int's are 4 bytes; char's are 1. (Some people didn't seem to realize that a char* is 4 bytes rather than 1.)3. I think it would be helpful if you explained why, when using realloc(), it's a good idea to increase the allocated size multiplicatively rather than additively. Besides, everyone loves the "tearing down the hotel" metaphor. :) 4. If they use call-by-reference, they had better make sure that they keep the same reference. So if they pass in a pointer as an argument to a function, they shouldn't call malloc() or realloc() on that function. (Mention the double pointer as another option.) Most people will make this mistake eventually if no one warns them about it. When I was learning C, I sort of viewed malloc() and realloc() as magical memory-increasing functions; that is to say, I didn't think very hard about the meaning of assigning a pointer to malloc()'s return value. I suspect some of your students would benefit from having the details spelled out. (Or spelled out again, if you've already done that.) 5. It's possible to get through a lot (but not all) of the CS major without learning basic Unix shell syntax, but that's really just wasted time. Pipes, backgrounding, man, scp, and grep really help even at the intro level. I realize the purpose of the class isn't to teach Unix, but in past years I think there was a TA help session on these things. They don't need to know how to write their own Emacs modes, but the basics would definitely be helpful. 6. malloc/free -- If Valgrind/gdb reports a problem inside of malloc() or free(), chances are that the student has *not* discovered a bug in gcc. (I just heard how one of Zhong's students' proved the correctness of the libraries for his thesis; that's pretty cool.) Explain why you can't malloc() twice on the same pointer. Explain how with multidimensional pointers, you must malloc/free each dimension separately. Drill down the one-to-one correspondence between malloc'ing and free'ing.7. Null characters: It's not obvious to newbies that some library functions require them, particularly null-terminated strings. Tell them that char*'s must be null terminated in order for <string.h> functions to work.8. Off-by-one errors: Tell people that when all else fails, take a hard look at their comparison operators; i. e. make sure that > shouldn't really be a >=.9. This is probably another thing for a help session or workshop, but I feel almost everyone could benefit from basic software engineering methodology. Stylistic awkwardness I noticed: --Using a mess of if-then-else's instead of nested control structures. --Using while-loops with iterators that get initialized right before the beginning of the loop and get incremented with each iteration, when they could be using for-loops. --Doing the setup work for a loop right before the beginning of the loop and then at the end of every iteration, instead of at the beginning of every iteration. Conversely: doing the cleanup work at the beginning of every iteration and then after the loop has completed.10. Tell them to use assert(). (Frequently.) When you cover binary search, using placement of debugging statements in code in order to pin down an error might be an instructive example. 11. Tell them to use either printf statements or a debugger to debug. I think they can figure out how to do this on their own, they just need to be told it's a good idea.Hopefully some of these suggestions will be helpful. I won't be offended if you don't pass them on to your students, and I understand if you put a higher teaching priority on non-coding subjects, but all the things I mentioned were things I wish someone had told me. I'd be happy to run a help session on this stuff if you and the TAs are too busy, but otherwise I wouldn't presume. Best,JimThis was posted to Piazza by an anonymous student the day before the2018 Assignment 7 was due. The 223 staff do not necessarily endorse any of the sentiments hinted at in this document.
I would like to thank David Galles for making this site available and Xiao Shi for pointing me to it.↩
If anybody would like to send me more detailed advice on any of these topics, I’d be happy to paste it in below.↩
The compiler is GCC version 4.8.2-19ubuntu1 running on a Linux 3.13.0-44-generic kernel running inside VirtualBox on a Windows 8.1 machine with a 3.30-Ghz AMD FX-6100 CPU, so don’t be surprised if you get different numbers on a real machine.↩
The pattern here is thatHEAD is the most recent commit,HEAD^ the one before it,HEAD^^ the one before that, and so on. This is sometimes nicer than having to pull hex gibberish out of the output ofgit log.↩
Technically I can usegit reset to get rid of the commit, butgit reset can be dangerous, since it throws away information.↩
This convention was not always followed in the early days of computing. For example, thePDP-7 on which UNIX was first developed used 18-bit words, which conveniently translated into six octal digits back in the pre-hexadecimal era.↩
Certain ancient versions of C ran on machines with a different character set encoding, likeEBCDIC. The C standard does not guarantee ASCII encoding.↩
C++ programmers will prefer++x if they are not otherwise using the return value, because ifx is some very complicated type with overloaded++, using preincrement avoids having to save a copy of the old value.↩
Exception: Global variables and static local variables are guaranteed to be initialized to an all-0 pattern, which will give the value 0 for most types.↩
The reason for excludingchar * andvoid * is that these are often used to represent pointers to objects with arbitrary types.↩
In this case you will get lucky most of the time, since the odds are that malloc will give you a block that is slightly bigger thanstrlen(s) anyway. But bugs that only manifest themselves occasionally are even worse than bugs that break your program every time, because they are much harder to track down.↩
Some programs (e.g. /c/cs223/bin/submit) will use this to change their behavior depending on what name you call them with.↩
There are various ways to work around this. The simplest is to put aunion inside a largerstruct that includes an explicit type tag.↩
Arguably, this is a bug in the design of the language: if the compiler knows thatsp has typestruct string *, there is no particular reason why it can’t interpretsp.length assp->length. But it doesn’t do this, so you will have to remember to writesp->length instead.↩
This is also the simplest way to deal with the inconsistencies between different compilers in how they handle inline functions. For an extensive discussion of the terrifying portability issues that arise in pre-C99 C compilers, seehttp://www.greenend.org.uk/rjk/tech/inline.html.↩
To make the example work, we are violating our usual rule of always using braces inif statements.↩
The# operator looks like it ought to be useful here, but it only works for expanding arguments to macros and not for expanding macros themselves. Attempting to get around this by wrappingMESSAGE in a macro that applies the# operator to its first argument will end in tears ifMESSAGE contains any special characters like commas or right parentheses. The C preprocessor has many unfortunate limitations.↩
This is an abuse of notation, where the equals sign is really acting like set membership. The general rule is that an expressionO(f(n)) = O(g(n)) is true if for any choice of a function inO(f(n)), that function is inO(g(n)). This relation is transitive and symmetric, but unlike real equality it’s not symmetric.↩
A small child of my acquaintance once explained that this wouldn’t work, because you would hit your head on the ceiling.↩
The example below uses theoffsetof macro, defined instddef.h, to allocate a truncated head that doesn’t include this extra space. This is probably more trouble than it is worth in this case, but might be useful if we were creating a lot of empty heads and the contents were more than 4 bytes long.↩
A summary of the state of this problem as of 2013 can be found inhttp://arxiv.org/pdf/1306.0207v1.pdf.↩
This only works if the graph is undirected, which means that for every edgeuv there is a matching edgevu with the same weight.↩
But it’s linear in the numerical value of the output, which means thatfib(n) will actually terminate in a reasonable amount of time on a typical modern computer when run on anyn small enough thatF(n) fits in 32 bits. Running it using 64-bit (or larger) integer representations will be slower.↩
The actual analysis is pretty complicated, since we are more likely to land in a bigger pile, but it’s not hard to show that on average even the bigger pile has no more than 3/4 of the elements.↩
This otherwise bizarre-looking modification is useful for modeling scheduling problems, wherea+b is the time to doa andb in parallel, anda*b is the time to doa andb sequentially. The reason for making the first case+ and the second case* is because this makes the distributive lawa*(b+c) = (a*b)+(a*c) work. It also allows tricks like matrix multiplication using the standard definition. Seehttp://maxplus.org for more than you probably want to know about this.↩
Vaughn, M. (director),Kick-Ass, 2010.↩
Not intended as legal advice.↩
Stratfordians, Oxfordians, and other conspiracy theorists might object that these results depend critically on the precise formatting of the text. We counter this objection by observing that we used theProject Gutenberg e-text ofThe Tempest, which, while not necessarily the most favored by academic Shakespeare scholars, is the easiest version to obtain on-line. We consider it further evidence of Sir Francis Bacon’s genius that not only was he able to subtly encode his name throughout his many brilliant plays, but he was even able to anticipate the effects of modern spelling and punctuation on this encoding.↩
Normally this is a dangerous thing to assume, but this assignment is complicated enough already.↩